Add Parameter read_file_handle to InputFile (#4388)

This commit is contained in:
Bibo-Joshi 2024-08-02 22:28:38 +02:00 committed by GitHub
parent e637d1733c
commit 3a49372591
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 206 additions and 72 deletions

View file

@ -1305,8 +1305,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args: Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
photo (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ photo (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
:class:`telegram.PhotoSize`): Photo to send. | :class:`pathlib.Path` | :class:`telegram.PhotoSize`): Photo to send.
|fileinput| |fileinput|
Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
@ -1465,9 +1465,9 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args: Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
audio (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ audio (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:class:`telegram.Audio`): Audio file to send. :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Audio`): Audio file to
|fileinput| send. |fileinput|
Lastly you can pass an existing :class:`telegram.Audio` object to send. Lastly you can pass an existing :class:`telegram.Audio` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
@ -1617,8 +1617,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args: Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
document (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ document (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:class:`telegram.Document`): File to send. :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Document`): File to send.
|fileinput| |fileinput|
Lastly you can pass an existing :class:`telegram.Document` object to send. Lastly you can pass an existing :class:`telegram.Document` object to send.
@ -1755,8 +1755,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args: Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:class:`telegram.Sticker`): Sticker to send. :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Sticker`): Sticker to send.
|fileinput| Video stickers can only be sent by a ``file_id``. Video and animated |fileinput| Video stickers can only be sent by a ``file_id``. Video and animated
stickers can't be sent via an HTTP URL. stickers can't be sent via an HTTP URL.
@ -1895,8 +1895,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args: Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
video (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ video (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
:class:`telegram.Video`): Video file to send. | :class:`pathlib.Path` | :class:`telegram.Video`): Video file to send.
|fileinput| |fileinput|
Lastly you can pass an existing :class:`telegram.Video` object to send. Lastly you can pass an existing :class:`telegram.Video` object to send.
@ -2059,8 +2059,9 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args: Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
video_note (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ video_note (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:class:`telegram.VideoNote`): Video note to send. :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.VideoNote`): Video note
to send.
Pass a file_id as String to send a video note that exists on the Telegram 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. servers (recommended) or upload a new video using multipart/form-data.
|uploadinput| |uploadinput|
@ -2209,9 +2210,9 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args: Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
animation (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ animation (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:class:`telegram.Animation`): Animation to send. :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Animation`): Animation to
|fileinput| send. |fileinput|
Lastly you can pass an existing :class:`telegram.Animation` object to send. Lastly you can pass an existing :class:`telegram.Animation` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
@ -2371,8 +2372,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args: Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
voice (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ voice (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
:class:`telegram.Voice`): Voice file to send. | :class:`pathlib.Path` | :class:`telegram.Voice`): Voice file to send.
|fileinput| |fileinput|
Lastly you can pass an existing :class:`telegram.Voice` object to send. Lastly you can pass an existing :class:`telegram.Voice` object to send.
@ -6370,8 +6371,9 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Args: Args:
user_id (:obj:`int`): User identifier of sticker file owner. user_id (:obj:`int`): User identifier of sticker file owner.
sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
A file with the sticker in the ``".WEBP"``, ``".PNG"``, ``".TGS"`` or ``".WEBM"`` :obj:`bytes` | :class:`pathlib.Path`): A file with the sticker in the
``".WEBP"``, ``".PNG"``, ``".TGS"`` or ``".WEBM"``
format. See `here <https://core.telegram.org/stickers>`_ for technical requirements format. See `here <https://core.telegram.org/stickers>`_ for technical requirements
. |uploadinput| . |uploadinput|
@ -6695,8 +6697,9 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionadded:: 21.1 .. versionadded:: 21.1
thumbnail (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \ thumbnail (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
optional): A **.WEBP** or **.PNG** image with the thumbnail, must :obj:`bytes` | :class:`pathlib.Path`, optional): A **.WEBP** or **.PNG** image
with the thumbnail, must
be up to :tg-const:`telegram.constants.StickerSetLimit.MAX_STATIC_THUMBNAIL_SIZE` be up to :tg-const:`telegram.constants.StickerSetLimit.MAX_STATIC_THUMBNAIL_SIZE`
kilobytes in size and have width and height of exactly kilobytes in size and have width and height of exactly
:tg-const:`telegram.constants.StickerSetLimit.STATIC_THUMB_DIMENSIONS` px, or a :tg-const:`telegram.constants.StickerSetLimit.STATIC_THUMB_DIMENSIONS` px, or a

View file

@ -22,7 +22,7 @@ import mimetypes
from typing import IO, Optional, Union from typing import IO, Optional, Union
from uuid import uuid4 from uuid import uuid4
from telegram._utils.files import load_file from telegram._utils.files import guess_file_name, load_file
from telegram._utils.strings import TextEncoding from telegram._utils.strings import TextEncoding
from telegram._utils.types import FieldTuple from telegram._utils.types import FieldTuple
@ -53,9 +53,36 @@ class InputFile:
attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in
the request to Telegram should point to the multipart data via an ``attach://`` URI. the request to Telegram should point to the multipart data via an ``attach://`` URI.
Defaults to `False`. Defaults to `False`.
read_file_handle (:obj:`bool`, optional): If :obj:`True` and :paramref:`obj` is a file
handle, the data will be read from the file handle on initialization of this object.
If :obj:`False`, the file handle will be passed on to the
`networking backend <telegram.request.BaseRequest.do_request>`_ which will have to
handle the reading. Defaults to :obj:`True`.
Tip:
If you upload extremely large files, you may want to set this to :obj:`False` to
avoid reading the complete file into memory. Additionally, this may be supported
better by the networking backend (in particular it is handled better by
the default :class:`~telegram.request.HTTPXRequest`).
Important:
If you set this to :obj:`False`, you have to ensure that the file handle is still
open when the request is made. In particular, the following snippet can *not* work
as expected.
.. code-block:: python
with open('file.txt', 'rb') as file:
input_file = InputFile(file, read_file_handle=False)
# here the file handle is already closed and the upload will fail
await bot.send_document(chat_id, input_file)
.. versionadded:: NEXT.VERSION
Attributes: Attributes:
input_file_content (:obj:`bytes`): The binary content of the file to send. input_file_content (:obj:`bytes` | :class:`IO`): The binary content of the file to send.
attach_name (:obj:`str`): Optional. If present, the parameter this file belongs to in attach_name (:obj:`str`): Optional. If present, the parameter this file belongs to in
the request to Telegram should point to the multipart data via a an URI of the form the request to Telegram should point to the multipart data via a an URI of the form
``attach://<attach_name>`` URI. ``attach://<attach_name>`` URI.
@ -71,14 +98,18 @@ class InputFile:
obj: Union[IO[bytes], bytes, str], obj: Union[IO[bytes], bytes, str],
filename: Optional[str] = None, filename: Optional[str] = None,
attach: bool = False, attach: bool = False,
read_file_handle: bool = True,
): ):
if isinstance(obj, bytes): if isinstance(obj, bytes):
self.input_file_content: bytes = obj self.input_file_content: Union[bytes, IO[bytes]] = obj
elif isinstance(obj, str): elif isinstance(obj, str):
self.input_file_content = obj.encode(TextEncoding.UTF_8) self.input_file_content = obj.encode(TextEncoding.UTF_8)
else: elif read_file_handle:
reported_filename, self.input_file_content = load_file(obj) reported_filename, self.input_file_content = load_file(obj)
filename = filename or reported_filename filename = filename or reported_filename
else:
self.input_file_content = obj
filename = filename or guess_file_name(obj)
self.attach_name: Optional[str] = "attached" + uuid4().hex if attach else None self.attach_name: Optional[str] = "attached" + uuid4().hex if attach else None
@ -95,8 +126,11 @@ class InputFile:
def field_tuple(self) -> FieldTuple: def field_tuple(self) -> FieldTuple:
"""Field tuple representing the contents of the file for upload to the Telegram servers. """Field tuple representing the contents of the file for upload to the Telegram servers.
.. versionchanged:: NEXT.VERSION
Content may now be a file handle.
Returns: Returns:
Tuple[:obj:`str`, :obj:`bytes`, :obj:`str`]: Tuple[:obj:`str`, :obj:`bytes` | :class:`IO`, :obj:`str`]:
""" """
return self.filename, self.input_file_content, self.mimetype return self.filename, self.input_file_content, self.mimetype

View file

@ -50,8 +50,8 @@ class InputMedia(TelegramObject):
Args: Args:
media_type (:obj:`str`): Type of media that the instance represents. media_type (:obj:`str`): Type of media that the instance represents.
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`telegram.Animation` | :class:`telegram.Audio` | \ :class:`pathlib.Path` | :class:`telegram.Animation` | :class:`telegram.Audio` | \
:class:`telegram.Document` | :class:`telegram.PhotoSize` | \ :class:`telegram.Document` | :class:`telegram.PhotoSize` | \
:class:`telegram.Video`): File to send. :class:`telegram.Video`): File to send.
|fileinputnopath| |fileinputnopath|
@ -128,8 +128,9 @@ class InputPaidMedia(TelegramObject):
Args: Args:
type (:obj:`str`): Type of media that the instance represents. type (:obj:`str`): Type of media that the instance represents.
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`telegram.PhotoSize` | :class:`telegram.Video`): File to send. |fileinputnopath| :class:`pathlib.Path` | :class:`telegram.PhotoSize` | :class:`telegram.Video`): File
to send. |fileinputnopath|
Lastly you can pass an existing telegram media object of the corresponding type Lastly you can pass an existing telegram media object of the corresponding type
to send. to send.
@ -167,8 +168,8 @@ class InputPaidMediaPhoto(InputPaidMedia):
.. versionadded:: 21.4 .. versionadded:: 21.4
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`telegram.PhotoSize`): File to send. |fileinputnopath| :class:`pathlib.Path` | :class:`telegram.PhotoSize`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
Attributes: Attributes:
@ -207,8 +208,8 @@ class InputPaidMediaVideo(InputPaidMedia):
changed by Telegram. changed by Telegram.
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`telegram.Video`): File to send. |fileinputnopath| :class:`pathlib.Path` | :class:`telegram.Video`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Video` object to send. Lastly you can pass an existing :class:`telegram.Video` object to send.
thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
optional): |thumbdocstringnopath| optional): |thumbdocstringnopath|
@ -278,8 +279,8 @@ class InputMediaAnimation(InputMedia):
|removed_thumb_note| |removed_thumb_note|
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`telegram.Animation`): File to send. |fileinputnopath| :class:`pathlib.Path` | :class:`telegram.Animation`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Animation` object to send. Lastly you can pass an existing :class:`telegram.Animation` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
@ -401,8 +402,8 @@ class InputMediaPhoto(InputMedia):
.. seealso:: :wiki:`Working with Files and Media <Working-with-Files-and-Media>` .. seealso:: :wiki:`Working with Files and Media <Working-with-Files-and-Media>`
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`telegram.PhotoSize`): File to send. |fileinputnopath| :class:`pathlib.Path` | :class:`telegram.PhotoSize`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
@ -501,8 +502,8 @@ class InputMediaVideo(InputMedia):
|removed_thumb_note| |removed_thumb_note|
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`telegram.Video`): File to send. |fileinputnopath| :class:`pathlib.Path` | :class:`telegram.Video`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Video` object to send. Lastly you can pass an existing :class:`telegram.Video` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
@ -639,8 +640,8 @@ class InputMediaAudio(InputMedia):
|removed_thumb_note| |removed_thumb_note|
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`telegram.Audio`): File to send. |fileinputnopath| :class:`pathlib.Path` | :class:`telegram.Audio`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Audio` object to send. Lastly you can pass an existing :class:`telegram.Audio` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
@ -743,8 +744,8 @@ class InputMediaDocument(InputMedia):
|removed_thumb_note| |removed_thumb_note|
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
:class:`telegram.Document`): File to send. |fileinputnopath| | :class:`pathlib.Path` | :class:`telegram.Document`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Document` object to send. Lastly you can pass an existing :class:`telegram.Document` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2

View file

@ -41,7 +41,8 @@ class InputSticker(TelegramObject):
order of the arguments has changed. order of the arguments has changed.
Args: Args:
sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): The sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
| :class:`pathlib.Path`): The
added sticker. |uploadinputnopath| Animated and video stickers can't be uploaded via added sticker. |uploadinputnopath| Animated and video stickers can't be uploaded via
HTTP URL. HTTP URL.
emoji_list (Sequence[:obj:`str`]): Sequence of emoji_list (Sequence[:obj:`str`]): Sequence of

View file

@ -61,14 +61,21 @@ def load_file(
except AttributeError: except AttributeError:
return None, cast(Union[bytes, "InputFile", str, Path], obj) return None, cast(Union[bytes, "InputFile", str, Path], obj)
if hasattr(obj, "name") and not isinstance(obj.name, int): filename = guess_file_name(obj)
filename = Path(obj.name).name
else:
filename = None
return filename, contents return filename, contents
def guess_file_name(obj: FileInput) -> Optional[str]:
"""If the input is a file handle, read name and return it. Otherwise, return
the input unchanged.
"""
if hasattr(obj, "name") and not isinstance(obj.name, int):
return Path(obj.name).name
return None
def is_local_file(obj: Optional[FilePathInput]) -> bool: def is_local_file(obj: Optional[FilePathInput]) -> bool:
""" """
Checks if a given string is a file on local system. Checks if a given string is a file on local system.
@ -110,8 +117,8 @@ def parse_file_input( # pylint: disable=too-many-return-statements
attribute. attribute.
Args: Args:
file_input (:obj:`str` | :obj:`bytes` | :term:`file object` | Telegram media object): The file_input (:obj:`str` | :obj:`bytes` | :term:`file object` | :class:`~telegram.InputFile`\
input to parse. | Telegram media object): The input to parse.
tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g. tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g.
:class:`telegram.Animation`. :class:`telegram.Animation`.
filename (:obj:`str`, optional): The filename. Only relevant in case an filename (:obj:`str`, optional): The filename. Only relevant in case an

View file

@ -82,7 +82,7 @@ ReplyMarkup = Union[
.. versionadded:: 20.0 .. versionadded:: 20.0
""" """
FieldTuple = Tuple[str, bytes, str] FieldTuple = Tuple[str, Union[bytes, IO[bytes]], str]
"""Alias for return type of `InputFile.field_tuple`.""" """Alias for return type of `InputFile.field_tuple`."""
UploadFileDict = Dict[str, FieldTuple] UploadFileDict = Dict[str, FieldTuple]
"""Dictionary containing file data to be uploaded to the API.""" """Dictionary containing file data to be uploaded to the API."""

View file

@ -129,7 +129,11 @@ class RequestData:
@property @property
def multipart_data(self) -> UploadFileDict: def multipart_data(self) -> UploadFileDict:
"""Gives the files contained in this object as mapping of part name to encoded content.""" """Gives the files contained in this object as mapping of part name to encoded content.
.. versionchanged:: NEXT.VERSION
Content may now be a file handle.
"""
multipart_data: UploadFileDict = {} multipart_data: UploadFileDict = {}
for param in self._parameters: for param in self._parameters:
m_data = param.multipart_data m_data = param.multipart_data

View file

@ -77,7 +77,11 @@ class RequestParameter:
@property @property
def multipart_data(self) -> Optional[UploadFileDict]: def multipart_data(self) -> Optional[UploadFileDict]:
"""A dict with the file data to upload, if any.""" """A dict with the file data to upload, if any.
.. versionchanged:: NEXT.VERSION
Content may now be a file handle.
"""
if not self.input_files: if not self.input_files:
return None return None
return { return {

View file

@ -19,7 +19,7 @@
import contextlib import contextlib
import subprocess import subprocess
import sys import sys
from io import BytesIO from io import BufferedReader, BytesIO
import pytest import pytest
@ -66,21 +66,45 @@ class TestInputFileWithoutRequest:
assert input_file.attach_name is None assert input_file.attach_name is None
assert input_file.attach_uri is None assert input_file.attach_uri is None
def test_mimetypes(self): @pytest.mark.parametrize("read_file_handle", [True, False])
def test_mimetypes_file_handle(self, read_file_handle):
# Only test a few to make sure logic works okay # Only test a few to make sure logic works okay
assert InputFile(data_file("telegram.jpg").open("rb")).mimetype == "image/jpeg" assert (
InputFile(
data_file("telegram.jpg").open("rb"), read_file_handle=read_file_handle
).mimetype
== "image/jpeg"
)
# For some reason python can guess the type on macOS # For some reason python can guess the type on macOS
assert InputFile(data_file("telegram.webp").open("rb")).mimetype in [ assert InputFile(
data_file("telegram.webp").open("rb"), read_file_handle=read_file_handle
).mimetype in [
"application/octet-stream", "application/octet-stream",
"image/webp", "image/webp",
] ]
assert InputFile(data_file("telegram.mp3").open("rb")).mimetype == "audio/mpeg" assert (
InputFile(
data_file("telegram.mp3").open("rb"), read_file_handle=read_file_handle
).mimetype
== "audio/mpeg"
)
# For some reason windows drops the trailing i # For some reason windows drops the trailing i
assert InputFile(data_file("telegram.midi").open("rb")).mimetype in [ assert InputFile(
data_file("telegram.midi").open("rb"), read_file_handle=read_file_handle
).mimetype in [
"audio/mid", "audio/mid",
"audio/midi", "audio/midi",
] ]
# Test string file
assert (
InputFile(
data_file("text_file.txt").open("rb"), read_file_handle=read_file_handle
).mimetype
== "text/plain"
)
def test_mimetypes_other(self):
# Test guess from file # Test guess from file
assert InputFile(BytesIO(b"blah"), filename="tg.jpg").mimetype == "image/jpeg" assert InputFile(BytesIO(b"blah"), filename="tg.jpg").mimetype == "image/jpeg"
assert InputFile(BytesIO(b"blah"), filename="tg.mp3").mimetype == "audio/mpeg" assert InputFile(BytesIO(b"blah"), filename="tg.mp3").mimetype == "audio/mpeg"
@ -92,20 +116,49 @@ class TestInputFileWithoutRequest:
) )
assert InputFile(BytesIO(b"blah")).mimetype == "application/octet-stream" assert InputFile(BytesIO(b"blah")).mimetype == "application/octet-stream"
# Test string file @pytest.mark.parametrize("read_file_handle", [True, False])
assert InputFile(data_file("text_file.txt").open()).mimetype == "text/plain" def test_filenames(self, read_file_handle):
def test_filenames(self):
assert InputFile(data_file("telegram.jpg").open("rb")).filename == "telegram.jpg"
assert InputFile(data_file("telegram.jpg").open("rb"), filename="blah").filename == "blah"
assert ( assert (
InputFile(data_file("telegram.jpg").open("rb"), filename="blah.jpg").filename InputFile(
data_file("telegram.jpg").open("rb"), read_file_handle=read_file_handle
).filename
== "telegram.jpg"
)
assert (
InputFile(
data_file("telegram.jpg").open("rb"),
filename="blah",
read_file_handle=read_file_handle,
).filename
== "blah"
)
assert (
InputFile(
data_file("telegram.jpg").open("rb"),
filename="blah.jpg",
read_file_handle=read_file_handle,
).filename
== "blah.jpg" == "blah.jpg"
) )
assert InputFile(data_file("telegram").open("rb")).filename == "telegram"
assert InputFile(data_file("telegram").open("rb"), filename="blah").filename == "blah"
assert ( assert (
InputFile(data_file("telegram").open("rb"), filename="blah.jpg").filename == "blah.jpg" InputFile(data_file("telegram").open("rb"), read_file_handle=read_file_handle).filename
== "telegram"
)
assert (
InputFile(
data_file("telegram").open("rb"),
filename="blah",
read_file_handle=read_file_handle,
).filename
== "blah"
)
assert (
InputFile(
data_file("telegram").open("rb"),
filename="blah.jpg",
read_file_handle=read_file_handle,
).filename
== "blah.jpg"
) )
class MockedFileobject: class MockedFileobject:
@ -140,6 +193,19 @@ class TestInputFileWithoutRequest:
== "blah.jpg" == "blah.jpg"
) )
@pytest.mark.parametrize("read_file_handle", [True, False])
def test_read_file_handle(self, read_file_handle):
input_file = InputFile(
data_file("telegram.jpg").open("rb"), read_file_handle=read_file_handle
)
content = input_file.field_tuple[1]
if read_file_handle:
assert isinstance(content, bytes)
assert content == data_file("telegram.jpg").read_bytes()
else:
assert isinstance(content, BufferedReader)
assert content.read() == data_file("telegram.jpg").read_bytes()
class TestInputFileWithRequest: class TestInputFileWithRequest:
async def test_send_bytes(self, bot, chat_id): async def test_send_bytes(self, bot, chat_id):

View file

@ -30,6 +30,7 @@ import httpx
import pytest import pytest
from httpx import AsyncHTTPTransport from httpx import AsyncHTTPTransport
from telegram import InputFile
from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.strings import TextEncoding from telegram._utils.strings import TextEncoding
from telegram.error import ( from telegram.error import (
@ -48,6 +49,7 @@ from telegram.request._httpxrequest import HTTPXRequest
from telegram.request._requestparameter import RequestParameter from telegram.request._requestparameter import RequestParameter
from telegram.warnings import PTBDeprecationWarning from telegram.warnings import PTBDeprecationWarning
from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.envvars import TEST_WITH_OPT_DEPS
from tests.auxil.files import data_file
from tests.auxil.networking import NonchalantHttpxRequest from tests.auxil.networking import NonchalantHttpxRequest
from tests.auxil.slots import mro_slots from tests.auxil.slots import mro_slots
@ -821,3 +823,15 @@ class TestHTTPXRequestWithRequest:
task_2.exception() task_2.exception()
except (asyncio.CancelledError, asyncio.InvalidStateError): except (asyncio.CancelledError, asyncio.InvalidStateError):
pass pass
async def test_input_file_postponed_read(self, bot, chat_id):
"""Here we test that `read_file_handle=False` is correctly handled by HTTPXRequest.
Since manually building the RequestData object has no real benefit, we simply use the Bot
for that.
"""
message = await bot.send_document(
document=InputFile(data_file("telegram.jpg").open("rb"), read_file_handle=False),
chat_id=chat_id,
)
assert message.document
assert message.document.file_name == "telegram.jpg"

View file

@ -117,7 +117,7 @@ PTB_EXTRA_PARAMS = {
"PassportElementError": {"source", "type", "message"}, "PassportElementError": {"source", "type", "message"},
"InputMedia": {"caption", "caption_entities", "media", "media_type", "parse_mode"}, "InputMedia": {"caption", "caption_entities", "media", "media_type", "parse_mode"},
"InputMedia(Animation|Audio|Document|Photo|Video|VideoNote|Voice)": {"filename"}, "InputMedia(Animation|Audio|Document|Photo|Video|VideoNote|Voice)": {"filename"},
"InputFile": {"attach", "filename", "obj"}, "InputFile": {"attach", "filename", "obj", "read_file_handle"},
"MaybeInaccessibleMessage": {"date", "message_id", "chat"}, # attributes common to all subcls "MaybeInaccessibleMessage": {"date", "message_id", "chat"}, # attributes common to all subcls
"ChatBoostSource": {"source"}, # attributes common to all subclasses "ChatBoostSource": {"source"}, # attributes common to all subclasses
"MessageOrigin": {"type", "date"}, # attributes common to all subclasses "MessageOrigin": {"type", "date"}, # attributes common to all subclasses