From 242847dda43f9601259523d6326a0a0fb6d0bc73 Mon Sep 17 00:00:00 2001 From: poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Tue, 24 Nov 2020 19:31:32 +0100 Subject: [PATCH 1/9] initial commit --- telegram/bot.py | 11 +++++----- telegram/files/file.py | 42 ++++++++++++++++++++++++++------------- telegram/utils/helpers.py | 10 ++++++++++ 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 46a77b39791..631f193c573 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -94,7 +94,7 @@ ) from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.error import InvalidToken, TelegramError -from telegram.utils.helpers import DEFAULT_NONE, DefaultValue, to_timestamp +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue, to_timestamp, local_check from telegram.utils.request import Request from telegram.utils.types import FileLike, JSONDict @@ -2114,10 +2114,11 @@ def get_file( result = self._post('getFile', data, timeout=timeout, api_kwargs=api_kwargs) - if result.get('file_path'): # type: ignore - result['file_path'] = '{}/{}'.format( # type: ignore - self.base_file_url, result['file_path'] # type: ignore - ) + if not local_check(result.get('file_path')): # type: ignore + if result.get('file_path'): # type: ignore + result['file_path'] = '{}/{}'.format( # type: ignore + self.base_file_url, result['file_path'] # type: ignore + ) return File.de_json(result, self) # type: ignore diff --git a/telegram/files/file.py b/telegram/files/file.py index 7a6d9917f8d..1b358f3bd74 100644 --- a/telegram/files/file.py +++ b/telegram/files/file.py @@ -25,6 +25,7 @@ from telegram import TelegramObject from telegram.passport.credentials import decrypt +from telegram.utils.helpers import local_check if TYPE_CHECKING: from telegram import Bot, FileCredentials @@ -119,15 +120,23 @@ def download( if custom_path is not None and out is not None: raise ValueError('custom_path and out are mutually exclusive') - # Convert any UTF-8 char into a url encoded ASCII string. - url = self._get_encoded_url() + local_file = local_check(self.file_path) + + if local_file: + url = self.file_path + else: + # Convert any UTF-8 char into a url encoded ASCII string. + url = self._get_encoded_url() if out: - buf = self.bot.request.retrieve(url) - if self._credentials: - buf = decrypt( - b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf - ) + if local_file: + buf = open(url, "rb").read() + else: + buf = self.bot.request.retrieve(url) + if self._credentials: + buf = decrypt( + b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf + ) out.write(buf) return out @@ -138,11 +147,14 @@ def download( else: filename = os.path.join(os.getcwd(), self.file_id) - buf = self.bot.request.retrieve(url, timeout=timeout) - if self._credentials: - buf = decrypt( - b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf - ) + if local_file: + buf = open(url, "rb").read() + else: + buf = self.bot.request.retrieve(url, timeout=timeout) + if self._credentials: + buf = decrypt( + b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf + ) with open(filename, 'wb') as fobj: fobj.write(buf) return filename @@ -169,8 +181,10 @@ def download_as_bytearray(self, buf: bytearray = None) -> bytes: """ if buf is None: buf = bytearray() - - buf.extend(self.bot.request.retrieve(self._get_encoded_url())) + if local_check(self.file_path): + buf.extend(open(self.file_path, "rb").read()) + else: + buf.extend(self.bot.request.retrieve(self._get_encoded_url())) return buf def set_credentials(self, credentials: 'FileCredentials') -> None: diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py index a736a75fb1b..2b03b6e7696 100644 --- a/telegram/utils/helpers.py +++ b/telegram/utils/helpers.py @@ -26,6 +26,7 @@ from collections import defaultdict from html import escape from numbers import Number +from pathlib import Path from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Optional, Tuple, Union @@ -55,6 +56,15 @@ def get_signal_name(signum: int) -> str: return _signames[signum] +def local_check(file_path: str) -> bool: + """Checks if a given file path exists on the local system""" + if str(file_path).startswith('file://'): + return True + if Path(file_path).exists(): + return True + return False + + def escape_markdown(text: str, version: int = 1, entity_type: str = None) -> str: """ Helper function to escape telegram markup symbols. From 661109b271b3bb3a996b036ae114fb12542d7c09 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Tue, 24 Nov 2020 21:56:35 +0100 Subject: [PATCH 2/9] Some tweaking & tests --- telegram/bot.py | 13 ++++++----- telegram/files/file.py | 15 ++++++++----- telegram/utils/helpers.py | 26 ++++++++++++++++------ tests/data/local_file.txt | 1 + tests/test_bot.py | 18 +++++++++++++++ tests/test_file.py | 47 +++++++++++++++++++++++++++++++++++++++ tests/test_helpers.py | 19 ++++++++++++++++ 7 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 tests/data/local_file.txt diff --git a/telegram/bot.py b/telegram/bot.py index 631f193c573..9f08037a085 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -94,7 +94,7 @@ ) from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.error import InvalidToken, TelegramError -from telegram.utils.helpers import DEFAULT_NONE, DefaultValue, to_timestamp, local_check +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue, to_timestamp, is_local_file from telegram.utils.request import Request from telegram.utils.types import FileLike, JSONDict @@ -2114,11 +2114,12 @@ def get_file( result = self._post('getFile', data, timeout=timeout, api_kwargs=api_kwargs) - if not local_check(result.get('file_path')): # type: ignore - if result.get('file_path'): # type: ignore - result['file_path'] = '{}/{}'.format( # type: ignore - self.base_file_url, result['file_path'] # type: ignore - ) + if result.get('file_path') and not is_local_file( # type: ignore[union-attr] + result['file_path'] # type: ignore[index] + ): + result['file_path'] = '{}/{}'.format( # type: ignore[index] + self.base_file_url, result['file_path'] # type: ignore[index] + ) return File.de_json(result, self) # type: ignore diff --git a/telegram/files/file.py b/telegram/files/file.py index 1b358f3bd74..5da74a1c36f 100644 --- a/telegram/files/file.py +++ b/telegram/files/file.py @@ -25,7 +25,7 @@ from telegram import TelegramObject from telegram.passport.credentials import decrypt -from telegram.utils.helpers import local_check +from telegram.utils.helpers import is_local_file if TYPE_CHECKING: from telegram import Bot, FileCredentials @@ -99,7 +99,10 @@ def download( the ``out.write`` method. Note: - :attr:`custom_path` and :attr:`out` are mutually exclusive. + * :attr:`custom_path` and :attr:`out` are mutually exclusive. + * If neither :attr:`custom_path` nor :attr:`out` is provided and :attr:`file_path` is + the path of a local file (as is the case for local Bot API Servers running in the + local mode), this method will just return the path. Args: custom_path (:obj:`str`, optional): Custom path. @@ -111,7 +114,7 @@ def download( Returns: :obj:`str` | :obj:`io.BufferedWriter`: The same object as :attr:`out` if specified. - Otherwise, returns the filename downloaded to. + Otherwise, returns the filename downloaded to or the file path of the local file. Raises: ValueError: If both :attr:`custom_path` and :attr:`out` are passed. @@ -120,7 +123,7 @@ def download( if custom_path is not None and out is not None: raise ValueError('custom_path and out are mutually exclusive') - local_file = local_check(self.file_path) + local_file = is_local_file(self.file_path) if local_file: url = self.file_path @@ -142,6 +145,8 @@ def download( if custom_path: filename = custom_path + elif local_file: + return self.file_path elif self.file_path: filename = basename(self.file_path) else: @@ -181,7 +186,7 @@ def download_as_bytearray(self, buf: bytearray = None) -> bytes: """ if buf is None: buf = bytearray() - if local_check(self.file_path): + if is_local_file(self.file_path): buf.extend(open(self.file_path, "rb").read()) else: buf.extend(self.bot.request.retrieve(self._get_encoded_url())) diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py index 2b03b6e7696..7c088d2d38e 100644 --- a/telegram/utils/helpers.py +++ b/telegram/utils/helpers.py @@ -56,13 +56,25 @@ def get_signal_name(signum: int) -> str: return _signames[signum] -def local_check(file_path: str) -> bool: - """Checks if a given file path exists on the local system""" - if str(file_path).startswith('file://'): - return True - if Path(file_path).exists(): - return True - return False +def is_local_file(string: Optional[str], absolute: bool = True) -> bool: + """ + Checks if a given string is a file on local system. + + Args: + string (:obj:`str`): The string to check. + absolute (:obj:`bool`): Optional. Whether to allow only absolute paths. Defaults to + :obj:`True`. + """ + if string is None: + return False + + path = Path(string) + try: + if path.exists() and path.is_file(): + return path.is_absolute() if absolute else True + return False + except Exception: + return False def escape_markdown(text: str, version: int = 1, entity_type: str = None) -> str: diff --git a/tests/data/local_file.txt b/tests/data/local_file.txt new file mode 100644 index 00000000000..85d6e1c2f61 --- /dev/null +++ b/tests/data/local_file.txt @@ -0,0 +1 @@ +Saint-Saƫns \ No newline at end of file diff --git a/tests/test_bot.py b/tests/test_bot.py index b87f479d96d..4f4f10e9e06 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -18,6 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. import time import datetime as dtm +from pathlib import Path from platform import python_implementation import pytest @@ -747,6 +748,23 @@ def test_get_one_user_profile_photo(self, bot, chat_id): assert user_profile_photos.photos[0][0].file_size == 5403 # get_file is tested multiple times in the test_*media* modules. + # Here we only test the behaviour for bot apis in local mode + def test_get_file_local_mode(self, bot, monkeypatch): + path = str(Path.cwd() / 'tests' / 'data' / 'game.gif') + + def _post(*args, **kwargs): + return { + 'file_id': None, + 'file_unique_id': None, + 'file_size': None, + 'file_path': path, + } + + monkeypatch.setattr(bot, '_post', _post) + + resulting_path = bot.get_file('file_id').file_path + assert bot.token not in resulting_path + assert resulting_path == path # TODO: Needs improvement. No feasable way to test until bots can add members. def test_kick_chat_member(self, monkeypatch, bot): diff --git a/tests/test_file.py b/tests/test_file.py index a4e0556041d..35281c599f2 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import os +from pathlib import Path from tempfile import TemporaryFile, mkstemp import pytest @@ -37,6 +38,17 @@ def file(bot): ) +@pytest.fixture(scope='class') +def local_file(bot): + return File( + TestFile.file_id, + TestFile.file_unique_id, + file_path=str(Path.cwd() / 'tests' / 'data' / 'local_file.txt'), + file_size=TestFile.file_size, + bot=bot, + ) + + class TestFile: file_id = 'NOTVALIDDOESNOTMATTER' file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' @@ -88,6 +100,9 @@ def test(*args, **kwargs): finally: os.unlink(out_file) + def test_download_local_file(self, local_file): + assert local_file.download() == local_file.file_path + def test_download_custom_path(self, monkeypatch, file): def test(*args, **kwargs): return self.file_content @@ -104,6 +119,18 @@ def test(*args, **kwargs): os.close(file_handle) os.unlink(custom_path) + def test_download_custom_path_local_file(self, local_file): + file_handle, custom_path = mkstemp() + try: + out_file = local_file.download(custom_path) + assert out_file == custom_path + + with open(out_file, 'rb') as fobj: + assert fobj.read() == self.file_content + finally: + os.close(file_handle) + os.unlink(custom_path) + def test_download_no_filename(self, monkeypatch, file): def test(*args, **kwargs): return self.file_content @@ -132,6 +159,14 @@ def test(*args, **kwargs): out_fobj.seek(0) assert out_fobj.read() == self.file_content + def test_download_file_obj_local_file(self, local_file): + with TemporaryFile() as custom_fobj: + out_fobj = local_file.download(out=custom_fobj) + assert out_fobj is custom_fobj + + out_fobj.seek(0) + assert out_fobj.read() == self.file_content + def test_download_bytearray(self, monkeypatch, file): def test(*args, **kwargs): return self.file_content @@ -149,6 +184,18 @@ def test(*args, **kwargs): assert buf2[len(buf) :] == buf assert buf2[: len(buf)] == buf + def test_download_bytearray_local_file(self, local_file): + # Check that a download to a newly allocated bytearray works. + buf = local_file.download_as_bytearray() + assert buf == bytearray(self.file_content) + + # Check that a download to a given bytearray works (extends the bytearray). + buf2 = buf[:] + buf3 = local_file.download_as_bytearray(buf=buf2) + assert buf3 is buf2 + assert buf2[len(buf) :] == buf + assert buf2[: len(buf)] == buf + def test_equality(self, bot): a = File(self.file_id, self.file_unique_id, bot) b = File('', self.file_unique_id, bot) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 3ef3270620f..83d59914653 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -18,6 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. import time import datetime as dtm +from pathlib import Path import pytest @@ -258,3 +259,21 @@ def test_mention_markdown_2(self): expected = r'[the\_name](tg://user?id=1)' assert expected == helpers.mention_markdown(1, 'the_name') + + @pytest.mark.parametrize( + 'string,absolute,expected', + [ + ('tests/data/game.gif', False, True), + ('tests/data', False, False), + ('tests/data/game.gif', True, False), + ('tests/data', True, False), + (str(Path.cwd() / 'tests' / 'data' / 'game.gif'), True, True), + (str(Path.cwd() / 'tests' / 'data'), True, False), + ('https:/api.org/file/botTOKEN/document/file_3', True, False), + ('https:/api.org/file/botTOKEN/document/file_3', False, False), + (None, True, False), + (None, False, False), + ], + ) + def test_is_local_file(self, string, absolute, expected): + assert helpers.is_local_file(string, absolute) == expected From aa2ff17558a0c17f3abfdea2c7d64c89e8d35311 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Wed, 25 Nov 2020 21:59:48 +0100 Subject: [PATCH 3/9] Handle local paths as file input and unify file input parsing --- telegram/bot.py | 149 +++++++++-------------------------- telegram/files/inputmedia.py | 69 ++++------------ telegram/utils/helpers.py | 56 ++++++++++++- tests/test_animation.py | 16 ++++ tests/test_audio.py | 15 ++++ tests/test_bot.py | 14 ++++ tests/test_document.py | 15 ++++ tests/test_helpers.py | 46 ++++++++++- tests/test_inputmedia.py | 42 ++++++++++ tests/test_photo.py | 15 ++++ tests/test_sticker.py | 73 +++++++++++++++++ tests/test_video.py | 15 ++++ tests/test_videonote.py | 15 ++++ tests/test_voice.py | 15 ++++ 14 files changed, 384 insertions(+), 171 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 9f08037a085..406eca0c9f0 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -36,7 +36,6 @@ Tuple, TypeVar, Union, - cast, no_type_check, ) @@ -63,7 +62,6 @@ File, GameHighScore, InlineQueryResult, - InputFile, InputMedia, LabeledPrice, Location, @@ -94,7 +92,13 @@ ) from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.error import InvalidToken, TelegramError -from telegram.utils.helpers import DEFAULT_NONE, DefaultValue, to_timestamp, is_local_file +from telegram.utils.helpers import ( + DEFAULT_NONE, + DefaultValue, + to_timestamp, + is_local_file, + parse_file_input, +) from telegram.utils.request import Request from telegram.utils.types import FileLike, JSONDict @@ -606,13 +610,7 @@ def send_photo( :class:`telegram.TelegramError` """ - if isinstance(photo, PhotoSize): - photo = photo.file_id - elif InputFile.is_file(photo): - photo = cast(IO, photo) - photo = InputFile(photo) # type: ignore[assignment] - - data: JSONDict = {'chat_id': chat_id, 'photo': photo} + data: JSONDict = {'chat_id': chat_id, 'photo': parse_file_input(photo, PhotoSize)} if caption: data['caption'] = caption @@ -702,13 +700,7 @@ def send_audio( :class:`telegram.TelegramError` """ - if isinstance(audio, Audio): - audio = audio.file_id - elif InputFile.is_file(audio): - audio = cast(IO, audio) - audio = InputFile(audio) - - data: JSONDict = {'chat_id': chat_id, 'audio': audio} + data: JSONDict = {'chat_id': chat_id, 'audio': parse_file_input(audio, Audio)} if duration: data['duration'] = duration @@ -721,10 +713,7 @@ def send_audio( if parse_mode: data['parse_mode'] = parse_mode if thumb: - if InputFile.is_file(thumb): - thumb = cast(IO, thumb) - thumb = InputFile(thumb, attach=True) - data['thumb'] = thumb + data['thumb'] = parse_file_input(thumb, attach=True) return self._message( # type: ignore[return-value] 'sendAudio', @@ -806,13 +795,10 @@ def send_document( :class:`telegram.TelegramError` """ - if isinstance(document, Document): - document = document.file_id - elif InputFile.is_file(document): - document = cast(IO, document) - document = InputFile(document, filename=filename) - - data: JSONDict = {'chat_id': chat_id, 'document': document} + data: JSONDict = { + 'chat_id': chat_id, + 'document': parse_file_input(document, Document, filename=filename), + } if caption: data['caption'] = caption @@ -821,10 +807,7 @@ def send_document( if disable_content_type_detection is not None: data['disable_content_type_detection'] = disable_content_type_detection if thumb: - if InputFile.is_file(thumb): - thumb = cast(IO, thumb) - thumb = InputFile(thumb, attach=True) - data['thumb'] = thumb + data['thumb'] = parse_file_input(thumb, attach=True) return self._message( # type: ignore[return-value] 'sendDocument', @@ -884,13 +867,7 @@ def send_sticker( :class:`telegram.TelegramError` """ - if isinstance(sticker, Sticker): - sticker = sticker.file_id - elif InputFile.is_file(sticker): - sticker = cast(IO, sticker) - sticker = InputFile(sticker) - - data: JSONDict = {'chat_id': chat_id, 'sticker': sticker} + data: JSONDict = {'chat_id': chat_id, 'sticker': parse_file_input(sticker, Sticker)} return self._message( # type: ignore[return-value] 'sendSticker', @@ -979,13 +956,7 @@ def send_video( :class:`telegram.TelegramError` """ - if isinstance(video, Video): - video = video.file_id - elif InputFile.is_file(video): - video = cast(IO, video) - video = InputFile(video) - - data: JSONDict = {'chat_id': chat_id, 'video': video} + data: JSONDict = {'chat_id': chat_id, 'video': parse_file_input(video, Video)} if duration: data['duration'] = duration @@ -1000,10 +971,7 @@ def send_video( if height: data['height'] = height if thumb: - if InputFile.is_file(thumb): - thumb = cast(IO, thumb) - thumb = InputFile(thumb, attach=True) - data['thumb'] = thumb + data['thumb'] = parse_file_input(thumb, attach=True) return self._message( # type: ignore[return-value] 'sendVideo', @@ -1078,23 +1046,17 @@ def send_video_note( :class:`telegram.TelegramError` """ - if isinstance(video_note, VideoNote): - video_note = video_note.file_id - elif InputFile.is_file(video_note): - video_note = cast(IO, video_note) - video_note = InputFile(video_note) - - data: JSONDict = {'chat_id': chat_id, 'video_note': video_note} + data: JSONDict = { + 'chat_id': chat_id, + 'video_note': parse_file_input(video_note, VideoNote), + } if duration is not None: data['duration'] = duration if length is not None: data['length'] = length if thumb: - if InputFile.is_file(thumb): - thumb = cast(IO, thumb) - thumb = InputFile(thumb, attach=True) - data['thumb'] = thumb + data['thumb'] = parse_file_input(thumb, attach=True) return self._message( # type: ignore[return-value] 'sendVideoNote', @@ -1176,13 +1138,7 @@ def send_animation( :class:`telegram.TelegramError` """ - if isinstance(animation, Animation): - animation = animation.file_id - elif InputFile.is_file(animation): - animation = cast(IO, animation) - animation = InputFile(animation) - - data: JSONDict = {'chat_id': chat_id, 'animation': animation} + data: JSONDict = {'chat_id': chat_id, 'animation': parse_file_input(animation, Animation)} if duration: data['duration'] = duration @@ -1191,10 +1147,7 @@ def send_animation( if height: data['height'] = height if thumb: - if InputFile.is_file(thumb): - thumb = cast(IO, thumb) - thumb = InputFile(thumb, attach=True) - data['thumb'] = thumb + data['thumb'] = parse_file_input(thumb, attach=True) if caption: data['caption'] = caption if parse_mode: @@ -1270,13 +1223,7 @@ def send_voice( :class:`telegram.TelegramError` """ - if isinstance(voice, Voice): - voice = voice.file_id - elif InputFile.is_file(voice): - voice = cast(IO, voice) - voice = InputFile(voice) - - data: JSONDict = {'chat_id': chat_id, 'voice': voice} + data: JSONDict = {'chat_id': chat_id, 'voice': parse_file_input(voice, Voice)} if duration: data['duration'] = duration @@ -2704,10 +2651,7 @@ def set_webhook( if url is not None: data['url'] = url if certificate: - if InputFile.is_file(certificate): - certificate = cast(IO, certificate) - certificate = InputFile(certificate) - data['certificate'] = certificate + data['certificate'] = parse_file_input(certificate) if max_connections is not None: data['max_connections'] = max_connections if allowed_updates is not None: @@ -3647,11 +3591,7 @@ def set_chat_photo( :class:`telegram.TelegramError` """ - if InputFile.is_file(photo): - photo = cast(IO, photo) - photo = InputFile(photo) - - data: JSONDict = {'chat_id': chat_id, 'photo': photo} + data: JSONDict = {'chat_id': chat_id, 'photo': parse_file_input(photo)} result = self._post('setChatPhoto', data, timeout=timeout, api_kwargs=api_kwargs) @@ -3943,10 +3883,7 @@ def upload_sticker_file( :class:`telegram.TelegramError` """ - if InputFile.is_file(png_sticker): - png_sticker = InputFile(png_sticker) # type: ignore[assignment,arg-type] - - data: JSONDict = {'user_id': user_id, 'png_sticker': png_sticker} + data: JSONDict = {'user_id': user_id, 'png_sticker': parse_file_input(png_sticker)} result = self._post('uploadStickerFile', data, timeout=timeout, api_kwargs=api_kwargs) @@ -4016,18 +3953,12 @@ def create_new_sticker_set( :class:`telegram.TelegramError` """ - if InputFile.is_file(png_sticker): - png_sticker = InputFile(png_sticker) # type: ignore[assignment,arg-type] - - if InputFile.is_file(tgs_sticker): - tgs_sticker = InputFile(tgs_sticker) # type: ignore[assignment,arg-type] - data: JSONDict = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis} if png_sticker is not None: - data['png_sticker'] = png_sticker + data['png_sticker'] = parse_file_input(png_sticker) if tgs_sticker is not None: - data['tgs_sticker'] = tgs_sticker + data['tgs_sticker'] = parse_file_input(tgs_sticker) if contains_masks is not None: data['contains_masks'] = contains_masks if mask_position is not None: @@ -4095,18 +4026,12 @@ def add_sticker_to_set( :class:`telegram.TelegramError` """ - if InputFile.is_file(png_sticker): - png_sticker = InputFile(png_sticker) # type: ignore[assignment,arg-type] - - if InputFile.is_file(tgs_sticker): - tgs_sticker = InputFile(tgs_sticker) # type: ignore[assignment,arg-type] - data: JSONDict = {'user_id': user_id, 'name': name, 'emojis': emojis} if png_sticker is not None: - data['png_sticker'] = png_sticker + data['png_sticker'] = parse_file_input(png_sticker) if tgs_sticker is not None: - data['tgs_sticker'] = tgs_sticker + data['tgs_sticker'] = parse_file_input(tgs_sticker) if mask_position is not None: # We need to_json() instead of to_dict() here, because we're sending a media # message here, which isn't json dumped by utils.request @@ -4212,12 +4137,10 @@ def set_sticker_set_thumb( :class:`telegram.TelegramError` """ + data: JSONDict = {'name': name, 'user_id': user_id} - if InputFile.is_file(thumb): - thumb = cast(IO, thumb) - thumb = InputFile(thumb) - - data: JSONDict = {'name': name, 'user_id': user_id, 'thumb': thumb} + if thumb is not None: + data['thumb'] = parse_file_input(thumb) result = self._post('setStickerSetThumb', data, timeout=timeout, api_kwargs=api_kwargs) diff --git a/telegram/files/inputmedia.py b/telegram/files/inputmedia.py index 2bae6cd0699..b490d447ad1 100644 --- a/telegram/files/inputmedia.py +++ b/telegram/files/inputmedia.py @@ -18,10 +18,10 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """Base class for Telegram InputMedia Objects.""" -from typing import IO, Union, cast +from typing import Union from telegram import Animation, Audio, Document, InputFile, PhotoSize, TelegramObject, Video -from telegram.utils.helpers import DEFAULT_NONE, DefaultValue +from telegram.utils.helpers import DEFAULT_NONE, DefaultValue, parse_file_input from telegram.utils.types import FileLike @@ -77,7 +77,7 @@ class InputMediaAnimation(InputMedia): def __init__( self, media: Union[str, FileLike, Animation], - thumb: FileLike = None, + thumb: Union[str, FileLike] = None, caption: str = None, parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, width: int = None, @@ -91,18 +91,11 @@ def __init__( self.width = media.width self.height = media.height self.duration = media.duration - elif InputFile.is_file(media): - media = cast(IO, media) - self.media = InputFile(media, attach=True) else: - self.media = media # type: ignore[assignment] + self.media = parse_file_input(media, attach=True) if thumb: - if InputFile.is_file(thumb): - thumb = cast(IO, thumb) - self.thumb = InputFile(thumb, attach=True) - else: - self.thumb = thumb # type: ignore[assignment] + self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption @@ -143,14 +136,7 @@ def __init__( parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, ): self.type = 'photo' - - if isinstance(media, PhotoSize): - self.media: Union[str, InputFile] = media.file_id - elif InputFile.is_file(media): - media = cast(IO, media) - self.media = InputFile(media, attach=True) - else: - self.media = media # type: ignore[assignment] + self.media = parse_file_input(media, PhotoSize, attach=True) if caption: self.caption = caption @@ -211,7 +197,7 @@ def __init__( duration: int = None, supports_streaming: bool = None, parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, - thumb: FileLike = None, + thumb: Union[str, FileLike] = None, ): self.type = 'video' @@ -220,18 +206,11 @@ def __init__( self.width = media.width self.height = media.height self.duration = media.duration - elif InputFile.is_file(media): - media = cast(IO, media) - self.media = InputFile(media, attach=True) else: - self.media = media # type: ignore[assignment] + self.media = parse_file_input(media, attach=True) if thumb: - if InputFile.is_file(thumb): - thumb = cast(IO, thumb) - self.thumb = InputFile(thumb, attach=True) - else: - self.thumb = thumb # type: ignore[assignment] + self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption @@ -289,7 +268,7 @@ class InputMediaAudio(InputMedia): def __init__( self, media: Union[str, FileLike, Audio], - thumb: FileLike = None, + thumb: Union[str, FileLike] = None, caption: str = None, parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, duration: int = None, @@ -303,18 +282,11 @@ def __init__( self.duration = media.duration self.performer = media.performer self.title = media.title - elif InputFile.is_file(media): - media = cast(IO, media) - self.media = InputFile(media, attach=True) else: - self.media = media # type: ignore[assignment] + self.media = parse_file_input(media, attach=True) if thumb: - if InputFile.is_file(thumb): - thumb = cast(IO, thumb) - self.thumb = InputFile(thumb, attach=True) - else: - self.thumb = thumb # type: ignore[assignment] + self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption @@ -363,27 +335,16 @@ class InputMediaDocument(InputMedia): def __init__( self, media: Union[str, FileLike, Document], - thumb: FileLike = None, + thumb: Union[str, FileLike] = None, caption: str = None, parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, disable_content_type_detection: bool = None, ): self.type = 'document' - - if isinstance(media, Document): - self.media: Union[str, InputFile] = media.file_id - elif InputFile.is_file(media): - media = cast(IO, media) - self.media = InputFile(media, attach=True) - else: - self.media = media # type: ignore[assignment] + self.media = parse_file_input(media, Document, attach=True) if thumb: - if InputFile.is_file(thumb): - thumb = cast(IO, thumb) - self.thumb = InputFile(thumb, attach=True) - else: - self.thumb = thumb # type: ignore[assignment] + self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py index 7c088d2d38e..69d22b00001 100644 --- a/telegram/utils/helpers.py +++ b/telegram/utils/helpers.py @@ -28,14 +28,14 @@ from numbers import Number from pathlib import Path -from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Optional, Tuple, Union, Type, cast, IO import pytz # pylint: disable=E0401 -from telegram.utils.types import JSONDict +from telegram.utils.types import JSONDict, FileLike if TYPE_CHECKING: - from telegram import MessageEntity + from telegram import MessageEntity, TelegramObject, InputFile try: import ujson as json @@ -77,6 +77,56 @@ def is_local_file(string: Optional[str], absolute: bool = True) -> bool: return False +def parse_file_input( + file_input: Union[str, FileLike, 'TelegramObject'], + tg_type: Type['TelegramObject'] = None, + attach: bool = None, + filename: str = None, +) -> Union[str, 'InputFile']: + """ + 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 IO input, returns an :class:`telegram.InputFile`. + * If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id`` + attribute. + + Args: + file_input (:obj:`str` | file like | Telegram media object): The input to parse. + tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g. + :class:`telegram.Animation`. + attach (:obj:`bool`, optional): Whether this file should be send as one file or is part of + a collection of files. Only relevant in case an :class:`telegram.InputFile` is + returned. + filename (:obj:`str`, optional): The filename. Only relevant in case an + :class:`telegram.InputFile` is returned. + + Returns: + :obj:`str` | :class:`telegram.InputFile`: The parsed input. + """ + # Importing on file-level yields cyclic Import Errors + from telegram import InputFile # pylint: disable=C0415 + + if isinstance(file_input, str): + if file_input.startswith('file://'): + out = file_input + elif is_local_file(file_input, absolute=True): + out = f'file://{file_input}' + elif is_local_file(file_input, absolute=False): + out = f'file://{Path(file_input).absolute()}' + else: + out = file_input + return out + if InputFile.is_file(file_input): + file_input = cast(IO, file_input) + return InputFile(file_input, attach=attach, filename=filename) + if tg_type and isinstance(file_input, tg_type): + return file_input.file_id # type: ignore[attr-defined] + return file_input # type: ignore[return-value] + + def escape_markdown(text: str, version: int = 1, entity_type: str = None) -> str: """ Helper function to escape telegram markup symbols. diff --git a/tests/test_animation.py b/tests/test_animation.py index fee205c7346..1c6660e0554 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -18,6 +18,8 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. import os +from pathlib import Path + import pytest from flaky import flaky @@ -162,6 +164,20 @@ def test_send_animation_default_parse_mode_3(self, default_bot, chat_id, animati assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('animation') == expected and data.get('thumb') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.send_animation(chat_id, file, thumb=file) + assert test_flag + @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.parametrize( diff --git a/tests/test_audio.py b/tests/test_audio.py index df1a53b0b72..d202ffbf6ce 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import os +from pathlib import Path import pytest from flaky import flaky @@ -187,6 +188,20 @@ def test_send_audio_default_parse_mode_3(self, default_bot, chat_id, audio_file, assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('audio') == expected and data.get('thumb') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.send_audio(chat_id, file, thumb=file) + assert test_flag + def test_de_json(self, bot, audio): json_dict = { 'file_id': self.audio_file_id, diff --git a/tests/test_bot.py b/tests/test_bot.py index 4f4f10e9e06..e058f30fa97 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1389,6 +1389,20 @@ def func(): with open('tests/data/telegram_test_channel.jpg', 'rb') as f: expect_bad_request(func, 'Type of file mismatch', 'Telegram did not accept the file.') + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('photo') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.set_chat_photo(chat_id, file) + assert test_flag + @flaky(3, 1) @pytest.mark.timeout(10) def test_delete_chat_photo(self, bot, channel_id): diff --git a/tests/test_document.py b/tests/test_document.py index aa4c25c0466..d354fc60f1b 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import os +from pathlib import Path import pytest from flaky import flaky @@ -220,6 +221,20 @@ def test_send_document_default_allow_sending_without_reply( chat_id, document, reply_to_message_id=reply_to_message.message_id ) + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('document') == expected and data.get('thumb') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.send_document(chat_id, file, thumb=file) + assert test_flag + def test_de_json(self, bot, document): json_dict = { 'file_id': self.document_file_id, diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 83d59914653..98fcd56c817 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -22,7 +22,7 @@ import pytest -from telegram import Sticker +from telegram import Sticker, InputFile, Animation from telegram import Update from telegram import User from telegram import MessageEntity @@ -277,3 +277,47 @@ def test_mention_markdown_2(self): ) def test_is_local_file(self, string, absolute, expected): assert helpers.is_local_file(string, absolute) == expected + + @pytest.mark.parametrize( + 'string,expected', + [ + ('tests/data/game.gif', 'file://' + str(Path.cwd() / 'tests' / 'data' / 'game.gif')), + ('tests/data', 'tests/data'), + ('file://foobar', 'file://foobar'), + ( + str(Path.cwd() / 'tests' / 'data' / 'game.gif'), + 'file://' + str(Path.cwd() / 'tests' / 'data' / 'game.gif'), + ), + (str(Path.cwd() / 'tests' / 'data'), str(Path.cwd() / 'tests' / 'data')), + ( + 'https:/api.org/file/botTOKEN/document/file_3', + 'https:/api.org/file/botTOKEN/document/file_3', + ), + ], + ) + def test_parse_file_input_string(self, string, expected): + assert helpers.parse_file_input(string) == expected + + def test_parse_file_input_file_like(self): + with open('tests/data/game.gif', 'rb') as file: + parsed = helpers.parse_file_input(file) + + assert isinstance(parsed, InputFile) + assert not parsed.attach + assert parsed.filename == 'game.gif' + + with open('tests/data/game.gif', 'rb') as file: + parsed = helpers.parse_file_input(file, attach=True, filename='test_file') + + assert isinstance(parsed, InputFile) + assert parsed.attach + assert parsed.filename == 'test_file' + + def test_parse_file_input_tg_object(self): + animation = Animation('file_id', 'unique_id', 1, 1, 1) + assert helpers.parse_file_input(animation, Animation) == 'file_id' + assert helpers.parse_file_input(animation, MessageEntity) is animation + + @pytest.mark.parametrize('obj', [{1: 2}, [1, 2], (1, 2)]) + def test_parse_file_input_other(self, obj): + assert helpers.parse_file_input(obj) is obj diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py index b4a040de803..4c8618ffaa0 100644 --- a/tests/test_inputmedia.py +++ b/tests/test_inputmedia.py @@ -16,6 +16,8 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +from pathlib import Path + import pytest from flaky import flaky @@ -156,6 +158,13 @@ def test_with_video_file(self, video_file): # noqa: F811 assert isinstance(input_media_video.media, InputFile) assert input_media_video.caption == "test 3" + def test_with_local_files(self): + input_media_video = InputMediaVideo( + 'tests/data/telegram.mp4', thumb='tests/data/telegram.jpg' + ) + assert input_media_video.media == 'file://' + str(Path.cwd() / 'tests/data/telegram.mp4') + assert input_media_video.thumb == 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + class TestInputMediaPhoto: type_ = "photo" @@ -190,6 +199,10 @@ def test_with_photo_file(self, photo_file): # noqa: F811 assert isinstance(input_media_photo.media, InputFile) assert input_media_photo.caption == "test 2" + def test_with_local_files(self): + input_media_photo = InputMediaPhoto('tests/data/telegram.mp4') + assert input_media_photo.media == 'file://' + str(Path.cwd() / 'tests/data/telegram.mp4') + class TestInputMediaAnimation: type_ = "animation" @@ -231,6 +244,17 @@ def test_with_animation_file(self, animation_file): # noqa: F811 assert isinstance(input_media_animation.media, InputFile) assert input_media_animation.caption == "test 2" + def test_with_local_files(self): + input_media_animation = InputMediaAnimation( + 'tests/data/telegram.mp4', thumb='tests/data/telegram.jpg' + ) + assert input_media_animation.media == 'file://' + str( + Path.cwd() / 'tests/data/telegram.mp4' + ) + assert input_media_animation.thumb == 'file://' + str( + Path.cwd() / 'tests/data/telegram.jpg' + ) + class TestInputMediaAudio: type_ = "audio" @@ -278,6 +302,13 @@ def test_with_audio_file(self, audio_file): # noqa: F811 assert isinstance(input_media_audio.media, InputFile) assert input_media_audio.caption == "test 3" + def test_with_local_files(self): + input_media_audio = InputMediaAudio( + 'tests/data/telegram.mp4', thumb='tests/data/telegram.jpg' + ) + assert input_media_audio.media == 'file://' + str(Path.cwd() / 'tests/data/telegram.mp4') + assert input_media_audio.thumb == 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + class TestInputMediaDocument: type_ = "document" @@ -322,6 +353,17 @@ def test_with_document_file(self, document_file): # noqa: F811 assert isinstance(input_media_document.media, InputFile) assert input_media_document.caption == "test 3" + def test_with_local_files(self): + input_media_document = InputMediaDocument( + 'tests/data/telegram.mp4', thumb='tests/data/telegram.jpg' + ) + assert input_media_document.media == 'file://' + str( + Path.cwd() / 'tests/data/telegram.mp4' + ) + assert input_media_document.thumb == 'file://' + str( + Path.cwd() / 'tests/data/telegram.jpg' + ) + @pytest.fixture(scope='function') # noqa: F811 def media_group(photo, thumb): # noqa: F811 diff --git a/tests/test_photo.py b/tests/test_photo.py index 696c352cb9b..cc3317b8614 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -18,6 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. import os from io import BytesIO +from pathlib import Path import pytest from flaky import flaky @@ -199,6 +200,20 @@ def test_send_photo_default_parse_mode_3(self, default_bot, chat_id, photo_file, assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('photo') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.send_photo(chat_id, file) + assert test_flag + @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.parametrize( diff --git a/tests/test_sticker.py b/tests/test_sticker.py index 906474b24e7..18c766db965 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import os +from pathlib import Path from time import sleep import pytest @@ -203,6 +204,20 @@ def test(url, data, **kwargs): message = bot.send_sticker(sticker=sticker, chat_id=chat_id) assert message + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('sticker') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.send_sticker(chat_id, file) + assert test_flag + @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.parametrize( @@ -433,6 +448,64 @@ def test_bot_methods_4_tgs(self, bot, animated_sticker_set): file_id = animated_sticker_set.stickers[-1].file_id assert bot.delete_sticker_from_set(file_id) + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('png_sticker') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.upload_sticker_file(chat_id, file) + assert test_flag + + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('png_sticker') == expected and data.get('tgs_sticker') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.create_new_sticker_set( + chat_id, 'name', 'title', 'emoji', png_sticker=file, tgs_sticker=file + ) + assert test_flag + + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('png_sticker') == expected and data.get('tgs_sticker') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.add_sticker_to_set(chat_id, 'name', 'emoji', png_sticker=file, tgs_sticker=file) + assert test_flag + + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('thumb') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.set_sticker_set_thumb('name', chat_id, thumb=file) + assert test_flag + def test_get_file_instance_method(self, monkeypatch, sticker): def test(*args, **kwargs): return args[1] == sticker.file_id diff --git a/tests/test_video.py b/tests/test_video.py index 3d3deb965ab..1941e041803 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import os +from pathlib import Path import pytest from flaky import flaky @@ -203,6 +204,20 @@ def test_send_video_default_parse_mode_3(self, default_bot, chat_id, video): assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('video') == expected and data.get('thumb') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.send_video(chat_id, file, thumb=file) + assert test_flag + @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.parametrize( diff --git a/tests/test_videonote.py b/tests/test_videonote.py index a33b83895d8..c1056951dda 100644 --- a/tests/test_videonote.py +++ b/tests/test_videonote.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import os +from pathlib import Path import pytest from flaky import flaky @@ -150,6 +151,20 @@ def test_to_dict(self, video_note): assert video_note_dict['duration'] == video_note.duration assert video_note_dict['file_size'] == video_note.file_size + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('video_note') == expected and data.get('thumb') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.send_video_note(chat_id, file, thumb=file) + assert test_flag + @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.parametrize( diff --git a/tests/test_voice.py b/tests/test_voice.py index 8f3fce4850f..cbd9be78c64 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import os +from pathlib import Path import pytest from flaky import flaky @@ -163,6 +164,20 @@ def test_send_voice_default_parse_mode_3(self, default_bot, chat_id, voice): assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) + 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 + expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + file = 'tests/data/telegram.jpg' + + def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + test_flag = data.get('voice') == expected + + monkeypatch.setattr(bot, '_post', make_assertion) + bot.send_voice(chat_id, file) + assert test_flag + @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.parametrize( From aaa7e9ec6d3907bc3bec771edfbe66a261715565 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Thu, 26 Nov 2020 00:46:20 +0100 Subject: [PATCH 4/9] Accept pathlib.Path objects for paths --- telegram/bot.py | 101 +++++++++++++++++++++-------------- telegram/files/inputmedia.py | 47 +++++++++------- telegram/utils/helpers.py | 32 +++++------ telegram/utils/types.py | 7 ++- tests/test_animation.py | 2 +- tests/test_audio.py | 2 +- tests/test_bot.py | 2 +- tests/test_document.py | 2 +- tests/test_helpers.py | 11 +++- tests/test_inputmedia.py | 10 ++-- tests/test_photo.py | 2 +- tests/test_sticker.py | 10 ++-- tests/test_video.py | 2 +- tests/test_videonote.py | 2 +- tests/test_voice.py | 2 +- 15 files changed, 138 insertions(+), 96 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 406eca0c9f0..872c26afeb0 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -100,7 +100,7 @@ parse_file_input, ) from telegram.utils.request import Request -from telegram.utils.types import FileLike, JSONDict +from telegram.utils.types import FileInput, JSONDict if TYPE_CHECKING: from telegram.ext import Defaults @@ -580,7 +580,8 @@ def send_photo( Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format @channelusername). - photo (:obj:`str` | `filelike object` | :class:`telegram.PhotoSize`): Photo to send. + photo (:obj:`str` | `filelike object` | :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 @@ -632,7 +633,7 @@ def send_photo( def send_audio( self, chat_id: Union[int, str], - audio: Union[str, Audio, FileLike], + audio: Union[FileInput, Audio], duration: int = None, performer: str = None, title: str = None, @@ -642,7 +643,7 @@ def send_audio( reply_markup: ReplyMarkup = None, timeout: float = 20, parse_mode: str = None, - thumb: FileLike = None, + thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: bool = None, ) -> Optional[Message]: @@ -662,7 +663,8 @@ def send_audio( Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format @channelusername). - audio (:obj:`str` | `filelike object` | :class:`telegram.Audio`): Audio file to send. + audio (:obj:`str` | `filelike object` | :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 @@ -684,7 +686,8 @@ def send_audio( reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if + 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 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. @@ -730,7 +733,7 @@ def send_audio( def send_document( self, chat_id: Union[int, str], - document: Union[str, Document, FileLike], + document: Union[FileInput, Document], filename: str = None, caption: str = None, disable_notification: bool = False, @@ -738,7 +741,7 @@ def send_document( reply_markup: ReplyMarkup = None, timeout: float = 20, parse_mode: str = None, - thumb: FileLike = None, + thumb: FileInput = None, api_kwargs: JSONDict = None, disable_content_type_detection: bool = None, allow_sending_without_reply: bool = None, @@ -756,7 +759,8 @@ def send_document( Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format @channelusername). - document (:obj:`str` | `filelike object` | :class:`telegram.Document`): File to send. + document (:obj:`str` | `filelike object` | :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 @@ -779,7 +783,8 @@ def send_document( reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if + 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 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. @@ -824,7 +829,7 @@ def send_document( def send_sticker( self, chat_id: Union[int, str], - sticker: Union[str, Sticker, FileLike], + sticker: Union[FileInput, Sticker], disable_notification: bool = False, reply_to_message_id: Union[int, str] = None, reply_markup: ReplyMarkup = None, @@ -842,7 +847,8 @@ def send_sticker( Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format @channelusername). - sticker (:obj:`str` | `filelike object` :class:`telegram.Sticker`): Sticker to send. + sticker (:obj:`str` | `filelike object` | :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 @@ -884,7 +890,7 @@ def send_sticker( def send_video( self, chat_id: Union[int, str], - video: Union[str, Video, FileLike], + video: Union[FileInput, Video], duration: int = None, caption: str = None, disable_notification: bool = False, @@ -895,7 +901,7 @@ def send_video( height: int = None, parse_mode: str = None, supports_streaming: bool = None, - thumb: FileLike = None, + thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: bool = None, ) -> Optional[Message]: @@ -916,7 +922,8 @@ def send_video( Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format @channelusername). - video (:obj:`str` | `filelike object` | :class:`telegram.Video`): Video file to send. + video (:obj:`str` | `filelike object` | :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 @@ -940,7 +947,8 @@ def send_video( reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if + 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 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. @@ -988,14 +996,14 @@ def send_video( def send_video_note( self, chat_id: Union[int, str], - video_note: Union[str, FileLike, VideoNote], + video_note: Union[FileInput, VideoNote], duration: int = None, length: int = None, disable_notification: bool = False, reply_to_message_id: Union[int, str] = None, reply_markup: ReplyMarkup = None, timeout: float = 20, - thumb: FileLike = None, + thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: bool = None, ) -> Optional[Message]: @@ -1013,7 +1021,8 @@ def send_video_note( Args: 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` | `filelike object` | :class:`telegram.VideoNote`): Video note + video_note (:obj:`str` | `filelike object` | :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 @@ -1030,7 +1039,8 @@ def send_video_note( reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if + 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 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. @@ -1073,11 +1083,11 @@ def send_video_note( def send_animation( self, chat_id: Union[int, str], - animation: Union[str, FileLike, Animation], + animation: Union[FileInput, Animation], duration: int = None, width: int = None, height: int = None, - thumb: FileLike = None, + thumb: FileInput = None, caption: str = None, parse_mode: str = None, disable_notification: bool = False, @@ -1100,7 +1110,8 @@ def send_animation( Args: 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` | `filelike object` | :class:`telegram.Animation`): Animation to + animation (:obj:`str` | `filelike object` | :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. @@ -1108,7 +1119,8 @@ def send_animation( duration (:obj:`int`, optional): Duration of sent animation in seconds. width (:obj:`int`, optional): Animation width. height (:obj:`int`, optional): Animation height. - thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if + 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 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. @@ -1168,7 +1180,7 @@ def send_animation( def send_voice( self, chat_id: Union[int, str], - voice: Union[str, FileLike, Voice], + voice: Union[FileInput, Voice], duration: int = None, caption: str = None, disable_notification: bool = False, @@ -1192,7 +1204,8 @@ def send_voice( Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format @channelusername). - voice (:obj:`str` | `filelike object` | :class:`telegram.Voice`): Voice file to send. + voice (:obj:`str` | `filelike object` | :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 @@ -2577,7 +2590,7 @@ def get_updates( def set_webhook( self, url: str = None, - certificate: FileLike = None, + certificate: FileInput = None, timeout: float = None, max_connections: int = 40, allowed_updates: List[str] = None, @@ -3565,7 +3578,7 @@ def export_chat_invite_link( def set_chat_photo( self, chat_id: Union[str, int], - photo: FileLike, + photo: FileInput, timeout: float = 20, api_kwargs: JSONDict = None, ) -> bool: @@ -3577,7 +3590,7 @@ def set_chat_photo( Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format @channelusername). - photo (`filelike object`): New chat photo. + photo (`filelike object` | :class:`pathlib.Path`): New chat photo. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). @@ -3852,7 +3865,7 @@ def get_sticker_set( def upload_sticker_file( self, user_id: Union[str, int], - png_sticker: Union[str, FileLike], + png_sticker: FileInput, timeout: float = 20, api_kwargs: JSONDict = None, ) -> File: @@ -3867,7 +3880,8 @@ def upload_sticker_file( Args: user_id (:obj:`int`): User identifier of sticker file owner. - png_sticker (:obj:`str` | `filelike object`): Png image with the sticker, + png_sticker (:obj:`str` | `filelike object` | :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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as @@ -3896,11 +3910,11 @@ def create_new_sticker_set( name: str, title: str, emojis: str, - png_sticker: Union[str, FileLike] = None, + png_sticker: FileInput = None, contains_masks: bool = None, mask_position: MaskPosition = None, timeout: float = 20, - tgs_sticker: Union[str, FileLike] = None, + tgs_sticker: FileInput = None, api_kwargs: JSONDict = None, ) -> bool: """ @@ -3925,13 +3939,15 @@ def create_new_sticker_set( must end in "_by_". is case insensitive. 1-64 characters. title (:obj:`str`): Sticker set title, 1-64 characters. - png_sticker (:obj:`str` | `filelike object`, optional): Png image with the sticker, + png_sticker (:obj:`str` | `filelike object` | :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. - tgs_sticker (:obj:`str` | `filelike object`, optional): TGS animation with the sticker, + tgs_sticker (:obj:`str` | `filelike object` | :class:`pathlib.Path`, optional): TGS + animation with the sticker, uploaded using multipart/form-data. See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements. @@ -3976,10 +3992,10 @@ def add_sticker_to_set( user_id: Union[str, int], name: str, emojis: str, - png_sticker: Union[str, FileLike] = None, + png_sticker: FileInput = None, mask_position: MaskPosition = None, timeout: float = 20, - tgs_sticker: Union[str, FileLike] = None, + tgs_sticker: FileInput = None, api_kwargs: JSONDict = None, ) -> bool: """ @@ -4000,13 +4016,15 @@ def add_sticker_to_set( Args: user_id (:obj:`int`): User identifier of created sticker set owner. name (:obj:`str`): Sticker set name. - png_sticker (:obj:`str` | `filelike object`, optional): PNG image with the sticker, + png_sticker (:obj:`str` | `filelike object` | :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. - tgs_sticker (:obj:`str` | `filelike object`, optional): TGS animation with the sticker, + tgs_sticker (:obj:`str` | `filelike object` | :class:`pathlib.Path`, optional): TGS + animation with the sticker, uploaded using multipart/form-data. See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements. @@ -4103,7 +4121,7 @@ def set_sticker_set_thumb( self, name: str, user_id: Union[str, int], - thumb: FileLike = None, + thumb: FileInput = None, timeout: float = None, api_kwargs: JSONDict = None, ) -> bool: @@ -4116,7 +4134,8 @@ def set_sticker_set_thumb( Args: name (:obj:`str`): Sticker set name user_id (:obj:`int`): User identifier of created sticker set owner. - thumb (:obj:`str` | `filelike object`, optional): A PNG image with the thumbnail, must + thumb (:obj:`str` | `filelike object` | :class:`pathlib.Path`, optional): A PNG image + with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a TGS animation with the thumbnail up to 32 kilobytes in size; see https://core.telegram.org/animated_stickers#technical-requirements for animated diff --git a/telegram/files/inputmedia.py b/telegram/files/inputmedia.py index b490d447ad1..b02eca6b2d6 100644 --- a/telegram/files/inputmedia.py +++ b/telegram/files/inputmedia.py @@ -22,7 +22,7 @@ from telegram import Animation, Audio, Document, InputFile, PhotoSize, TelegramObject, Video from telegram.utils.helpers import DEFAULT_NONE, DefaultValue, parse_file_input -from telegram.utils.types import FileLike +from telegram.utils.types import FileInput class InputMedia(TelegramObject): @@ -50,11 +50,13 @@ class InputMediaAnimation(InputMedia): Args: - media (:obj:`str` | `filelike object` | :class:`telegram.Animation`): File to send. Pass a + media (:obj:`str` | `filelike object` | :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. - thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if + 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 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. @@ -76,8 +78,8 @@ class InputMediaAnimation(InputMedia): def __init__( self, - media: Union[str, FileLike, Animation], - thumb: Union[str, FileLike] = None, + media: Union[FileInput, Animation], + thumb: FileInput = None, caption: str = None, parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, width: int = None, @@ -118,7 +120,8 @@ class InputMediaPhoto(InputMedia): parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. Args: - media (:obj:`str` | `filelike object` | :class:`telegram.PhotoSize`): File to send. Pass a + media (:obj:`str` | `filelike object` | :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. @@ -131,7 +134,7 @@ class InputMediaPhoto(InputMedia): def __init__( self, - media: Union[str, FileLike, PhotoSize], + media: Union[FileInput, PhotoSize], caption: str = None, parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, ): @@ -159,7 +162,8 @@ class InputMediaVideo(InputMedia): thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. Args: - media (:obj:`str` | `filelike object` | :class:`telegram.Video`): File to send. Pass a + media (:obj:`str` | `filelike object` | :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. @@ -173,7 +177,8 @@ class InputMediaVideo(InputMedia): duration (:obj:`int`, optional): Video duration. supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is suitable for streaming. - thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if + 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 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. @@ -190,14 +195,14 @@ class InputMediaVideo(InputMedia): def __init__( self, - media: Union[str, FileLike, Video], + media: Union[FileInput, Video], caption: str = None, width: int = None, height: int = None, duration: int = None, supports_streaming: bool = None, parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, - thumb: Union[str, FileLike] = None, + thumb: FileInput = None, ): self.type = 'video' @@ -240,7 +245,8 @@ class InputMediaAudio(InputMedia): thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. Args: - media (:obj:`str` | `filelike object` | :class:`telegram.Audio`): File to send. Pass a + media (:obj:`str` | `filelike object` | :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. @@ -253,7 +259,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 (`filelike object`, optional): Thumbnail of the file sent; can be ignored if + 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 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. @@ -267,8 +274,8 @@ class InputMediaAudio(InputMedia): def __init__( self, - media: Union[str, FileLike, Audio], - thumb: Union[str, FileLike] = None, + media: Union[FileInput, Audio], + thumb: FileInput = None, caption: str = None, parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, duration: int = None, @@ -313,7 +320,8 @@ class InputMediaDocument(InputMedia): the document is sent as part of an album. Args: - media (:obj:`str` | `filelike object` | :class:`telegram.Document`): File to send. Pass a + media (:obj:`str` | `filelike object` | :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. @@ -322,7 +330,8 @@ class InputMediaDocument(InputMedia): parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. - thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if + 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 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. @@ -334,8 +343,8 @@ class InputMediaDocument(InputMedia): def __init__( self, - media: Union[str, FileLike, Document], - thumb: Union[str, FileLike] = None, + media: Union[FileInput, Document], + thumb: FileInput = None, caption: str = None, parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, disable_content_type_detection: bool = None, diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py index 69d22b00001..85e984f118d 100644 --- a/telegram/utils/helpers.py +++ b/telegram/utils/helpers.py @@ -32,7 +32,7 @@ import pytz # pylint: disable=E0401 -from telegram.utils.types import JSONDict, FileLike +from telegram.utils.types import JSONDict, FileInput if TYPE_CHECKING: from telegram import MessageEntity, TelegramObject, InputFile @@ -56,19 +56,19 @@ def get_signal_name(signum: int) -> str: return _signames[signum] -def is_local_file(string: Optional[str], absolute: bool = True) -> bool: +def is_local_file(obj: Optional[Union[str, Path]], absolute: bool = True) -> bool: """ Checks if a given string is a file on local system. Args: - string (:obj:`str`): The string to check. + obj (:obj:`str`): The string to check. absolute (:obj:`bool`): Optional. Whether to allow only absolute paths. Defaults to :obj:`True`. """ - if string is None: + if obj is None: return False - path = Path(string) + path = Path(obj) if isinstance(obj, str) else obj try: if path.exists() and path.is_file(): return path.is_absolute() if absolute else True @@ -78,23 +78,24 @@ def is_local_file(string: Optional[str], absolute: bool = True) -> bool: def parse_file_input( - file_input: Union[str, FileLike, 'TelegramObject'], + file_input: Union[FileInput, 'TelegramObject'], tg_type: Type['TelegramObject'] = None, attach: bool = None, filename: str = None, -) -> Union[str, 'InputFile']: +) -> 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. + * :class:`pathlib.Path` objects are treated similarly as strings. * For IO input, returns an :class:`telegram.InputFile`. * If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id`` attribute. Args: - file_input (:obj:`str` | file like | Telegram media object): The input to parse. + file_input (:obj:`str` | `filelike object` | Telegram media object): The input to parse. tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g. :class:`telegram.Animation`. attach (:obj:`bool`, optional): Whether this file should be send as one file or is part of @@ -104,27 +105,28 @@ def parse_file_input( :class:`telegram.InputFile` is returned. Returns: - :obj:`str` | :class:`telegram.InputFile`: The parsed input. + :obj:`str` | :class:`telegram.InputFile` | :obj:`object`: The parsed input or the untouched + :attr:`file_input`, in case it's no valid file input. """ # Importing on file-level yields cyclic Import Errors from telegram import InputFile # pylint: disable=C0415 - if isinstance(file_input, str): - if file_input.startswith('file://'): - out = file_input - elif is_local_file(file_input, absolute=True): + if isinstance(file_input, str) and file_input.startswith('file://'): + return file_input + if isinstance(file_input, (str, Path)): + if is_local_file(file_input, absolute=True): out = f'file://{file_input}' elif is_local_file(file_input, absolute=False): out = f'file://{Path(file_input).absolute()}' else: - out = file_input + out = file_input # type: ignore[assignment] return out if InputFile.is_file(file_input): file_input = cast(IO, file_input) return InputFile(file_input, attach=attach, filename=filename) if tg_type and isinstance(file_input, tg_type): return file_input.file_id # type: ignore[attr-defined] - return file_input # type: ignore[return-value] + return file_input def escape_markdown(text: str, version: int = 1, entity_type: str = None) -> str: diff --git a/telegram/utils/types.py b/telegram/utils/types.py index 1ac4f6dcd0c..6100cd243b9 100644 --- a/telegram/utils/types.py +++ b/telegram/utils/types.py @@ -17,13 +17,18 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains custom typing aliases.""" +from pathlib import Path from typing import IO, TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypeVar, Union if TYPE_CHECKING: from telegram import InputFile, Update FileLike = Union[IO, 'InputFile'] -"""Either an open file handler or in :class:`telegram.InputFile`.""" +"""Either an open file handler or a :class:`telegram.InputFile`.""" + +FileInput = Union[str, FileLike, Path] +"""Valid input for passing files to Telegram. Either a file id as string, a file like object or +a local file path as string or :class:`pathlib.Path`.""" JSONDict = Dict[str, Any] """Dictionary containing response from Telegram or data to send to the API.""" diff --git a/tests/test_animation.py b/tests/test_animation.py index 1c6660e0554..cffbf40112f 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -167,7 +167,7 @@ def test_send_animation_default_parse_mode_3(self, default_bot, chat_id, animati 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): diff --git a/tests/test_audio.py b/tests/test_audio.py index d202ffbf6ce..873c17963d2 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -191,7 +191,7 @@ def test_send_audio_default_parse_mode_3(self, default_bot, chat_id, audio_file, 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): diff --git a/tests/test_bot.py b/tests/test_bot.py index e058f30fa97..dfa9fdad212 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1392,7 +1392,7 @@ def func(): 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): diff --git a/tests/test_document.py b/tests/test_document.py index d354fc60f1b..e7512d575a6 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -224,7 +224,7 @@ def test_send_document_default_allow_sending_without_reply( 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 98fcd56c817..8816575f45c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -269,6 +269,8 @@ def test_mention_markdown_2(self): ('tests/data', True, False), (str(Path.cwd() / 'tests' / 'data' / 'game.gif'), True, True), (str(Path.cwd() / 'tests' / 'data'), True, False), + (Path.cwd() / 'tests' / 'data' / 'game.gif', True, True), + (Path.cwd() / 'tests' / 'data', True, False), ('https:/api.org/file/botTOKEN/document/file_3', True, False), ('https:/api.org/file/botTOKEN/document/file_3', False, False), (None, True, False), @@ -281,14 +283,19 @@ def test_is_local_file(self, string, absolute, expected): @pytest.mark.parametrize( 'string,expected', [ - ('tests/data/game.gif', 'file://' + str(Path.cwd() / 'tests' / 'data' / 'game.gif')), + ('tests/data/game.gif', f"file://{Path.cwd() / 'tests' / 'data' / 'game.gif'}"), ('tests/data', 'tests/data'), ('file://foobar', 'file://foobar'), ( str(Path.cwd() / 'tests' / 'data' / 'game.gif'), - 'file://' + str(Path.cwd() / 'tests' / 'data' / 'game.gif'), + f"file://{Path.cwd() / 'tests' / 'data' / 'game.gif'}", ), (str(Path.cwd() / 'tests' / 'data'), str(Path.cwd() / 'tests' / 'data')), + ( + Path.cwd() / 'tests' / 'data' / 'game.gif', + f"file://{Path.cwd() / 'tests' / 'data' / 'game.gif'}", + ), + (Path.cwd() / 'tests' / 'data', Path.cwd() / 'tests' / 'data'), ( 'https:/api.org/file/botTOKEN/document/file_3', 'https:/api.org/file/botTOKEN/document/file_3', diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py index 4c8618ffaa0..742e3cf6c14 100644 --- a/tests/test_inputmedia.py +++ b/tests/test_inputmedia.py @@ -162,8 +162,8 @@ def test_with_local_files(self): input_media_video = InputMediaVideo( 'tests/data/telegram.mp4', thumb='tests/data/telegram.jpg' ) - assert input_media_video.media == 'file://' + str(Path.cwd() / 'tests/data/telegram.mp4') - assert input_media_video.thumb == 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + assert input_media_video.media == f"file://{Path.cwd() / 'tests/data/telegram.mp4'}" + assert input_media_video.thumb == f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" class TestInputMediaPhoto: @@ -201,7 +201,7 @@ def test_with_photo_file(self, photo_file): # noqa: F811 def test_with_local_files(self): input_media_photo = InputMediaPhoto('tests/data/telegram.mp4') - assert input_media_photo.media == 'file://' + str(Path.cwd() / 'tests/data/telegram.mp4') + assert input_media_photo.media == f"file://{Path.cwd() / 'tests/data/telegram.mp4'}" class TestInputMediaAnimation: @@ -306,8 +306,8 @@ def test_with_local_files(self): input_media_audio = InputMediaAudio( 'tests/data/telegram.mp4', thumb='tests/data/telegram.jpg' ) - assert input_media_audio.media == 'file://' + str(Path.cwd() / 'tests/data/telegram.mp4') - assert input_media_audio.thumb == 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + assert input_media_audio.media == f"file://{Path.cwd() / 'tests/data/telegram.mp4'}" + assert input_media_audio.thumb == f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" class TestInputMediaDocument: diff --git a/tests/test_photo.py b/tests/test_photo.py index cc3317b8614..665cbfd8d81 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -203,7 +203,7 @@ def test_send_photo_default_parse_mode_3(self, default_bot, chat_id, photo_file, 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): diff --git a/tests/test_sticker.py b/tests/test_sticker.py index 18c766db965..e107985a1d1 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -207,7 +207,7 @@ def test(url, data, **kwargs): 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): @@ -451,7 +451,7 @@ def test_bot_methods_4_tgs(self, bot, animated_sticker_set): 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): @@ -465,7 +465,7 @@ def make_assertion(_, data, *args, **kwargs): 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): @@ -481,7 +481,7 @@ def make_assertion(_, data, *args, **kwargs): 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): @@ -495,7 +495,7 @@ def make_assertion(_, data, *args, **kwargs): 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): diff --git a/tests/test_video.py b/tests/test_video.py index 1941e041803..ee0531903b2 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -207,7 +207,7 @@ def test_send_video_default_parse_mode_3(self, default_bot, chat_id, video): 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): diff --git a/tests/test_videonote.py b/tests/test_videonote.py index c1056951dda..517a07d0b2a 100644 --- a/tests/test_videonote.py +++ b/tests/test_videonote.py @@ -154,7 +154,7 @@ def test_to_dict(self, video_note): 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): diff --git a/tests/test_voice.py b/tests/test_voice.py index cbd9be78c64..a08c25010a7 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -167,7 +167,7 @@ def test_send_voice_default_parse_mode_3(self, default_bot, chat_id, voice): 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 - expected = 'file://' + str(Path.cwd() / 'tests/data/telegram.jpg') + expected = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}" file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): From 6a7e5adab0d9370b98a34c253f5d3fde050e1f1c Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sat, 28 Nov 2020 18:52:45 +0100 Subject: [PATCH 5/9] Some tweaks in download logic --- telegram/files/file.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/telegram/files/file.py b/telegram/files/file.py index 5da74a1c36f..2e41c596328 100644 --- a/telegram/files/file.py +++ b/telegram/files/file.py @@ -133,7 +133,8 @@ def download( if out: if local_file: - buf = open(url, "rb").read() + with open(url, 'rb') as file: + buf = file.read() else: buf = self.bot.request.retrieve(url) if self._credentials: @@ -152,14 +153,11 @@ def download( else: filename = os.path.join(os.getcwd(), self.file_id) - if local_file: - buf = open(url, "rb").read() - else: - buf = self.bot.request.retrieve(url, timeout=timeout) - if self._credentials: - buf = decrypt( - b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf - ) + buf = self.bot.request.retrieve(url, timeout=timeout) + if self._credentials: + buf = decrypt( + b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf + ) with open(filename, 'wb') as fobj: fobj.write(buf) return filename @@ -187,7 +185,8 @@ def download_as_bytearray(self, buf: bytearray = None) -> bytes: if buf is None: buf = bytearray() if is_local_file(self.file_path): - buf.extend(open(self.file_path, "rb").read()) + with open(self.file_path, "rb") as file: + buf.extend(file.read()) else: buf.extend(self.bot.request.retrieve(self._get_encoded_url())) return buf From 2d1f7bf2ba586c86dd7e65eacd9ea1e41f11eaf6 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sat, 28 Nov 2020 19:02:36 +0100 Subject: [PATCH 6/9] Tweak some more --- telegram/files/file.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/telegram/files/file.py b/telegram/files/file.py index 2e41c596328..f1ceb5076eb 100644 --- a/telegram/files/file.py +++ b/telegram/files/file.py @@ -153,11 +153,15 @@ def download( else: filename = os.path.join(os.getcwd(), self.file_id) - buf = self.bot.request.retrieve(url, timeout=timeout) - if self._credentials: - buf = decrypt( - b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf - ) + if local_file: + with open(url, 'rb') as file: + buf = file.read() + else: + buf = self.bot.request.retrieve(url, timeout=timeout) + if self._credentials: + buf = decrypt( + b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf + ) with open(filename, 'wb') as fobj: fobj.write(buf) return filename From 166611b3a35a3400a97866e0ae0e245a8e587fc0 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler Date: Sat, 28 Nov 2020 20:24:34 +0100 Subject: [PATCH 7/9] Address review --- telegram/files/file.py | 19 ++++++++++--------- telegram/utils/helpers.py | 14 ++++---------- tests/test_helpers.py | 26 +++++++++++--------------- 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/telegram/files/file.py b/telegram/files/file.py index f1ceb5076eb..68e6c3db71d 100644 --- a/telegram/files/file.py +++ b/telegram/files/file.py @@ -18,6 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram File.""" import os +import shutil import urllib.parse as urllib_parse from base64 import b64decode from os.path import basename @@ -144,6 +145,10 @@ def download( out.write(buf) return out + if custom_path and local_file: + shutil.copyfile(self.file_path, custom_path) + return custom_path + if custom_path: filename = custom_path elif local_file: @@ -153,15 +158,11 @@ def download( else: filename = os.path.join(os.getcwd(), self.file_id) - if local_file: - with open(url, 'rb') as file: - buf = file.read() - else: - buf = self.bot.request.retrieve(url, timeout=timeout) - if self._credentials: - buf = decrypt( - b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf - ) + buf = self.bot.request.retrieve(url, timeout=timeout) + if self._credentials: + buf = decrypt( + b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf + ) with open(filename, 'wb') as fobj: fobj.write(buf) return filename diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py index 85e984f118d..52c62efc5d6 100644 --- a/telegram/utils/helpers.py +++ b/telegram/utils/helpers.py @@ -56,23 +56,19 @@ def get_signal_name(signum: int) -> str: return _signames[signum] -def is_local_file(obj: Optional[Union[str, Path]], absolute: bool = True) -> bool: +def is_local_file(obj: Optional[Union[str, Path]]) -> bool: """ Checks if a given string is a file on local system. Args: obj (:obj:`str`): The string to check. - absolute (:obj:`bool`): Optional. Whether to allow only absolute paths. Defaults to - :obj:`True`. """ if obj is None: return False - path = Path(obj) if isinstance(obj, str) else obj + path = Path(obj) try: - if path.exists() and path.is_file(): - return path.is_absolute() if absolute else True - return False + return path.is_file() except Exception: return False @@ -114,9 +110,7 @@ def parse_file_input( if isinstance(file_input, str) and file_input.startswith('file://'): return file_input if isinstance(file_input, (str, Path)): - if is_local_file(file_input, absolute=True): - out = f'file://{file_input}' - elif is_local_file(file_input, absolute=False): + if is_local_file(file_input): out = f'file://{Path(file_input).absolute()}' else: out = file_input # type: ignore[assignment] diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 8816575f45c..ffdb928242c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -261,24 +261,20 @@ def test_mention_markdown_2(self): assert expected == helpers.mention_markdown(1, 'the_name') @pytest.mark.parametrize( - 'string,absolute,expected', + 'string,expected', [ - ('tests/data/game.gif', False, True), - ('tests/data', False, False), - ('tests/data/game.gif', True, False), - ('tests/data', True, False), - (str(Path.cwd() / 'tests' / 'data' / 'game.gif'), True, True), - (str(Path.cwd() / 'tests' / 'data'), True, False), - (Path.cwd() / 'tests' / 'data' / 'game.gif', True, True), - (Path.cwd() / 'tests' / 'data', True, False), - ('https:/api.org/file/botTOKEN/document/file_3', True, False), - ('https:/api.org/file/botTOKEN/document/file_3', False, False), - (None, True, False), - (None, False, False), + ('tests/data/game.gif', True), + ('tests/data', False), + (str(Path.cwd() / 'tests' / 'data' / 'game.gif'), True), + (str(Path.cwd() / 'tests' / 'data'), False), + (Path.cwd() / 'tests' / 'data' / 'game.gif', True), + (Path.cwd() / 'tests' / 'data', False), + ('https:/api.org/file/botTOKEN/document/file_3', False), + (None, False), ], ) - def test_is_local_file(self, string, absolute, expected): - assert helpers.is_local_file(string, absolute) == expected + def test_is_local_file(self, string, expected): + assert helpers.is_local_file(string) == expected @pytest.mark.parametrize( 'string,expected', From 03f05037026fb4361d555faab20756a6e9a20dee Mon Sep 17 00:00:00 2001 From: Bibo-Joshi Date: Sun, 29 Nov 2020 15:06:13 +0100 Subject: [PATCH 8/9] Update telegram/files/file.py Co-authored-by: Poolitzer <25934244+Poolitzer@users.noreply.github.com> --- telegram/files/file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/files/file.py b/telegram/files/file.py index 68e6c3db71d..b1a347501d7 100644 --- a/telegram/files/file.py +++ b/telegram/files/file.py @@ -102,7 +102,7 @@ def download( Note: * :attr:`custom_path` and :attr:`out` are mutually exclusive. * If neither :attr:`custom_path` nor :attr:`out` is provided and :attr:`file_path` is - the path of a local file (as is the case for local Bot API Servers running in the + the path of a local file (which is the case when a Bot API Server is running in local mode), this method will just return the path. Args: From 67f8918b52356154a3b564063cecc57341c9cc76 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi Date: Sun, 29 Nov 2020 15:07:14 +0100 Subject: [PATCH 9/9] Update telegram/utils/helpers.py Co-authored-by: Poolitzer <25934244+Poolitzer@users.noreply.github.com> --- telegram/utils/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py index 52c62efc5d6..4aa9d67d6c9 100644 --- a/telegram/utils/helpers.py +++ b/telegram/utils/helpers.py @@ -85,7 +85,7 @@ def parse_file_input( * 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. - * :class:`pathlib.Path` objects are treated similarly as strings. + * :class:`pathlib.Path` objects are treated the same way as strings. * For IO input, returns an :class:`telegram.InputFile`. * If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id`` attribute.