Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 92 additions & 33 deletions telegram/bot.py

Large diffs are not rendered by default.

16 changes: 10 additions & 6 deletions telegram/files/inputfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import logging
import mimetypes
import os
from typing import IO, Optional, Tuple
from typing import IO, Optional, Tuple, Union
from uuid import uuid4

DEFAULT_MIME_TYPE = 'application/octet-stream'
Expand All @@ -39,7 +39,8 @@ class InputFile:
attach (:obj:`str`): Optional. Attach id for sending multiple files.

Args:
obj (:obj:`File handler`): An open file descriptor.
obj (:obj:`File handler` | :obj:`bytes`): An open file descriptor or the files content as
bytes.
filename (:obj:`str`, optional): Filename for this InputFile.
attach (:obj:`bool`, optional): Whether this should be send as one file or is part of a
collection of files.
Expand All @@ -49,15 +50,18 @@ class InputFile:

"""

def __init__(self, obj: IO, filename: str = None, attach: bool = None):
def __init__(self, obj: Union[IO, bytes], filename: str = None, attach: bool = None):
self.filename = None
self.input_file_content = obj.read()
if isinstance(obj, bytes):
self.input_file_content = obj
else:
self.input_file_content = obj.read()
self.attach = 'attached' + uuid4().hex if attach else None

if filename:
self.filename = filename
elif hasattr(obj, 'name') and not isinstance(obj.name, int):
self.filename = os.path.basename(obj.name)
elif hasattr(obj, 'name') and not isinstance(obj.name, int): # type: ignore[union-attr]
self.filename = os.path.basename(obj.name) # type: ignore[union-attr]

image_mime_type = self.is_image(self.input_file_content)
if image_mime_type:
Expand Down
56 changes: 42 additions & 14 deletions telegram/files/inputmedia.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,28 @@ class InputMediaAnimation(InputMedia):


Args:
media (:obj:`str` | `filelike object` | :class:`pathlib.Path` | \
media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Animation`): File to send. Pass a
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.Animation` object to send.

.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
filename (:obj:`str`, optional): Custom file name for the animation, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.

.. versionadded:: 13.1
thumb (`filelike object` | :class:`pathlib.Path`, optional): Thumbnail of the file sent;
can be ignored if
thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of
the file sent; can be ignored if
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.

.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
caption (:obj:`str`, optional): Caption of the animation to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
Expand Down Expand Up @@ -155,11 +161,14 @@ class InputMediaPhoto(InputMedia):
entities that appear in the caption.

Args:
media (:obj:`str` | `filelike object` | :class:`pathlib.Path` | \
media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.PhotoSize`): File to send. Pass a
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.PhotoSize` object to send.

.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
filename (:obj:`str`, optional): Custom file name for the photo, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
Expand Down Expand Up @@ -209,11 +218,14 @@ class InputMediaVideo(InputMedia):
thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.

Args:
media (:obj:`str` | `filelike object` | :class:`pathlib.Path` | :class:`telegram.Video`):
File to send. Pass a
media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Video`): File to send. Pass a
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.Video` object to send.

.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
filename (:obj:`str`, optional): Custom file name for the video, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
Expand All @@ -231,13 +243,16 @@ 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` | :class:`pathlib.Path`, optional): Thumbnail of the file sent;
can be ignored if
thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of
the file sent; can be ignored if
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.

.. versionchanged:: 13.2
Accept :obj:`bytes` as input.

Note:
* When using a :class:`telegram.Video` for the :attr:`media` attribute. It will take the
width, height and duration from that video, unless otherwise specified with the optional
Expand Down Expand Up @@ -304,11 +319,15 @@ class InputMediaAudio(InputMedia):
thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.

Args:
media (:obj:`str` | `filelike object` | :class:`pathlib.Path` | :class:`telegram.Audio`):
media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Audio`):
File to send. Pass a
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.Audio` object to send.

.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
filename (:obj:`str`, optional): Custom file name for the audio, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
Expand All @@ -325,13 +344,16 @@ 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` | :class:`pathlib.Path`, optional): Thumbnail of the file sent;
can be ignored if
thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of
the file sent; can be ignored if
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.

.. versionchanged:: 13.2
Accept :obj:`bytes` as input.

Note:
When using a :class:`telegram.Audio` for the :attr:`media` attribute. It will take the
duration, performer and title from that video, unless otherwise specified with the
Expand Down Expand Up @@ -391,11 +413,14 @@ class InputMediaDocument(InputMedia):
the document is sent as part of an album.

Args:
media (:obj:`str` | `filelike object` | :class:`pathlib.Path` | \
media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Document`): File to send. Pass a
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.Document` object to send.

.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
filename (:obj:`str`, optional): Custom file name for the document, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
Expand All @@ -408,12 +433,15 @@ class InputMediaDocument(InputMedia):
in :class:`telegram.ParseMode` for the available modes.
caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
entities that appear in the caption, which can be specified instead of parse_mode.
thumb (`filelike object` | :class:`pathlib.Path`, optional): Thumbnail of the file sent;
can be ignored if
thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of
the file sent; can be ignored if
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.

.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
disable_content_type_detection (:obj:`bool`, optional): Disables automatic server-side
content type detection for files uploaded using multipart/form-data. Always true, if
the document is sent as part of an album.
Expand Down
9 changes: 6 additions & 3 deletions telegram/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,13 @@ def parse_file_input(
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 the same way as strings.
* For IO input, returns an :class:`telegram.InputFile`.
* For IO and bytes input, returns an :class:`telegram.InputFile`.
* If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id``
attribute.

Args:
file_input (:obj:`str` | `filelike object` | Telegram media object): The input to parse.
file_input (:obj:`str` | :obj:`bytes` | `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
Expand All @@ -111,10 +112,12 @@ def parse_file_input(
return file_input
if isinstance(file_input, (str, Path)):
if is_local_file(file_input):
out = f'file://{Path(file_input).absolute()}'
out = Path(file_input).absolute().as_uri()
else:
out = file_input # type: ignore[assignment]
return out
if isinstance(file_input, bytes):
return InputFile(file_input, attach=attach, filename=filename)
if InputFile.is_file(file_input):
file_input = cast(IO, file_input)
return InputFile(file_input, attach=attach, filename=filename)
Expand Down
6 changes: 3 additions & 3 deletions telegram/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
FileLike = Union[IO, '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`."""
FileInput = Union[str, bytes, FileLike, Path]
"""Valid input for passing files to Telegram. Either a file id as string, a file like object,
a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes`."""

JSONDict = Dict[str, Any]
"""Dictionary containing response from Telegram or data to send to the API."""
Expand Down
3 changes: 2 additions & 1 deletion tests/test_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,12 @@ 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 = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}"
expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri()
file = 'tests/data/telegram.jpg'

def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag
print(data.get('animation'), expected)
test_flag = data.get('animation') == expected and data.get('thumb') == expected

monkeypatch.setattr(bot, '_post', make_assertion)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,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 = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}"
expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri()
file = 'tests/data/telegram.jpg'

def make_assertion(_, data, *args, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,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 = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}"
expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri()
file = 'tests/data/telegram.jpg'

def make_assertion(_, data, *args, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,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 = f"file://{Path.cwd() / 'tests/data/telegram.jpg'}"
expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri()
file = 'tests/data/telegram.jpg'

def make_assertion(_, data, *args, **kwargs):
Expand Down
21 changes: 18 additions & 3 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,17 +279,17 @@ def test_is_local_file(self, string, expected):
@pytest.mark.parametrize(
'string,expected',
[
('tests/data/game.gif', f"file://{Path.cwd() / 'tests' / 'data' / 'game.gif'}"),
('tests/data/game.gif', (Path.cwd() / 'tests' / 'data' / 'game.gif').as_uri()),
('tests/data', 'tests/data'),
('file://foobar', 'file://foobar'),
(
str(Path.cwd() / 'tests' / 'data' / 'game.gif'),
f"file://{Path.cwd() / 'tests' / 'data' / 'game.gif'}",
(Path.cwd() / 'tests' / 'data' / 'game.gif').as_uri(),
),
(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' / 'game.gif').as_uri(),
),
(Path.cwd() / 'tests' / 'data', Path.cwd() / 'tests' / 'data'),
(
Expand All @@ -316,6 +316,21 @@ def test_parse_file_input_file_like(self):
assert parsed.attach
assert parsed.filename == 'test_file'

def test_parse_file_input_bytes(self):
with open('tests/data/text_file.txt', 'rb') as file:
parsed = helpers.parse_file_input(file.read())

assert isinstance(parsed, InputFile)
assert not parsed.attach
assert parsed.filename == 'application.octet-stream'

with open('tests/data/text_file.txt', 'rb') as file:
parsed = helpers.parse_file_input(file.read(), 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'
Expand Down
11 changes: 11 additions & 0 deletions tests/test_inputfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,14 @@ def read(self):
InputFile(MockedFileobject('tests/data/telegram'), filename='blah.jpg').filename
== 'blah.jpg'
)

def test_send_bytes(self, bot, chat_id):
# We test this here and not at the respective test modules because it's not worth
# duplicating the test for the different methods
with open('tests/data/text_file.txt', 'rb') as file:
message = bot.send_document(chat_id, file.read())

out = BytesIO()
assert message.document.get_file().download(out=out)
out.seek(0)
assert out.read().decode('utf-8') == 'PTB Rocks!'
Loading