mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2024-12-22 14:35:00 +01:00
Add Shortcut Parameters caption
, parse_mode
and caption_entities
to Bot.send_media_group
(#3295)
This commit is contained in:
parent
636654cb71
commit
d2c6c4b369
9 changed files with 223 additions and 9 deletions
|
@ -35,6 +35,7 @@ The following wonderful people contributed directly or indirectly to this projec
|
|||
- `daimajia <https://github.com/daimajia>`_
|
||||
- `Daniel Reed <https://github.com/nmlorg>`_
|
||||
- `D David Livingston <https://github.com/daviddl9>`_
|
||||
- `Dmitry Kolomatskiy <https://github.com/lemontree210>`_
|
||||
- `DonalDuck004 <https://github.com/DonalDuck004>`_
|
||||
- `Eana Hufwe <https://github.com/blueset>`_
|
||||
- `Ehsan Online <https://github.com/ehsanonline>`_
|
||||
|
|
|
@ -19,11 +19,11 @@
|
|||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an object that represents a Telegram Bot."""
|
||||
import asyncio
|
||||
import copy
|
||||
import functools
|
||||
import logging
|
||||
import pickle
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from copy import copy
|
||||
from datetime import datetime
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
|
@ -348,12 +348,12 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
|||
# 1)
|
||||
if isinstance(val, InputMedia):
|
||||
# Copy object as not to edit it in-place
|
||||
val = copy(val)
|
||||
val = copy.copy(val)
|
||||
val.parse_mode = DefaultValue.get_value(val.parse_mode)
|
||||
data[key] = val
|
||||
elif key == "media" and isinstance(val, list):
|
||||
# Copy objects as not to edit them in-place
|
||||
copy_list = [copy(media) for media in val]
|
||||
copy_list = [copy.copy(media) for media in val]
|
||||
for media in copy_list:
|
||||
media.parse_mode = DefaultValue.get_value(media.parse_mode)
|
||||
data[key] = copy_list
|
||||
|
@ -2005,9 +2005,17 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
|||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
caption: Optional[str] = None,
|
||||
parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
|
||||
) -> List[Message]:
|
||||
"""Use this method to send a group of photos or videos as an album.
|
||||
|
||||
Note:
|
||||
If you supply a :paramref:`caption` (along with either
|
||||
:paramref:`parse_mode` or :paramref:`caption_entities`),
|
||||
then items in :paramref:`media` must have no captions, and vice verca.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_media_group`,
|
||||
:attr:`telegram.Chat.send_media_group`,
|
||||
:attr:`telegram.User.send_media_group`
|
||||
|
@ -2044,6 +2052,18 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
|||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
|
||||
Telegram API.
|
||||
caption (:obj:`str`, optional): Caption that will be added to the
|
||||
first element of :paramref:`media`, so that it will be used as caption for the
|
||||
whole media group.
|
||||
Defaults to :obj:`None`.
|
||||
parse_mode (:obj:`str` | :obj:`None`, optional):
|
||||
Parse mode for :paramref:`caption`.
|
||||
See the constants in :class:`telegram.constants.ParseMode` for the
|
||||
available modes.
|
||||
caption_entities (List[:class:`telegram.MessageEntity`], optional):
|
||||
List of special entities for :paramref:`caption`,
|
||||
which can be specified instead of :paramref:`parse_mode`.
|
||||
Defaults to :obj:`None`.
|
||||
|
||||
Returns:
|
||||
List[:class:`telegram.Message`]: An array of the sent Messages.
|
||||
|
@ -2051,6 +2071,29 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
|||
Raises:
|
||||
:class:`telegram.error.TelegramError`
|
||||
"""
|
||||
if caption and any(
|
||||
[
|
||||
any(item.caption for item in media),
|
||||
any(item.caption_entities for item in media),
|
||||
# if parse_mode was set explicitly, even to None, error must be raised
|
||||
any(item.parse_mode is not DEFAULT_NONE for item in media),
|
||||
]
|
||||
):
|
||||
raise ValueError("You can only supply either group caption or media with captions.")
|
||||
|
||||
if caption:
|
||||
# Copy first item (to avoid mutation of original object), apply group caption to it.
|
||||
# This will lead to the group being shown with this caption.
|
||||
item_to_get_caption = copy.copy(media[0])
|
||||
item_to_get_caption.caption = caption
|
||||
if parse_mode is not DEFAULT_NONE:
|
||||
item_to_get_caption.parse_mode = parse_mode
|
||||
item_to_get_caption.caption_entities = caption_entities
|
||||
|
||||
# copy the list (just the references) to avoid mutating the original list
|
||||
media = media[:]
|
||||
media[0] = item_to_get_caption
|
||||
|
||||
data: JSONDict = {
|
||||
"chat_id": chat_id,
|
||||
"media": media,
|
||||
|
@ -2870,22 +2913,22 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
|||
# Copy the objects that need modification to avoid modifying the original object
|
||||
copied = False
|
||||
if hasattr(res, "parse_mode"):
|
||||
res = copy(res)
|
||||
res = copy.copy(res)
|
||||
copied = True
|
||||
res.parse_mode = DefaultValue.get_value(res.parse_mode)
|
||||
if hasattr(res, "input_message_content") and res.input_message_content:
|
||||
if hasattr(res.input_message_content, "parse_mode"):
|
||||
if not copied:
|
||||
res = copy(res)
|
||||
res = copy.copy(res)
|
||||
copied = True
|
||||
res.input_message_content = copy(res.input_message_content)
|
||||
res.input_message_content = copy.copy(res.input_message_content)
|
||||
res.input_message_content.parse_mode = DefaultValue.get_value(
|
||||
res.input_message_content.parse_mode
|
||||
)
|
||||
if hasattr(res.input_message_content, "disable_web_page_preview"):
|
||||
if not copied:
|
||||
res = copy(res)
|
||||
res.input_message_content = copy(res.input_message_content)
|
||||
res = copy.copy(res)
|
||||
res.input_message_content = copy.copy(res.input_message_content)
|
||||
res.input_message_content.disable_web_page_preview = DefaultValue.get_value(
|
||||
res.input_message_content.disable_web_page_preview
|
||||
)
|
||||
|
|
|
@ -1234,6 +1234,9 @@ class Chat(TelegramObject):
|
|||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
caption: Optional[str] = None,
|
||||
parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
|
||||
) -> List["Message"]:
|
||||
"""Shortcut for::
|
||||
|
||||
|
@ -1257,6 +1260,9 @@ class Chat(TelegramObject):
|
|||
api_kwargs=api_kwargs,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
protect_content=protect_content,
|
||||
caption=caption,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
)
|
||||
|
||||
async def send_chat_action(
|
||||
|
|
|
@ -1011,6 +1011,9 @@ class Message(TelegramObject):
|
|||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
caption: Optional[str] = None,
|
||||
parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
|
||||
) -> List["Message"]:
|
||||
"""Shortcut for::
|
||||
|
||||
|
@ -1043,6 +1046,9 @@ class Message(TelegramObject):
|
|||
api_kwargs=api_kwargs,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
protect_content=protect_content,
|
||||
caption=caption,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
)
|
||||
|
||||
async def reply_photo(
|
||||
|
|
|
@ -474,6 +474,9 @@ class User(TelegramObject):
|
|||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
caption: Optional[str] = None,
|
||||
parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
|
||||
) -> List["Message"]:
|
||||
"""Shortcut for::
|
||||
|
||||
|
@ -497,6 +500,9 @@ class User(TelegramObject):
|
|||
api_kwargs=api_kwargs,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
protect_content=protect_content,
|
||||
caption=caption,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
)
|
||||
|
||||
async def send_audio(
|
||||
|
|
|
@ -2259,6 +2259,9 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
rate_limit_args: RLARGS = None,
|
||||
caption: Optional[str] = None,
|
||||
parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
|
||||
) -> List[Message]:
|
||||
return await super().send_media_group(
|
||||
chat_id=chat_id,
|
||||
|
@ -2272,6 +2275,9 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
|
||||
caption=caption,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
)
|
||||
|
||||
async def send_message(
|
||||
|
|
|
@ -719,6 +719,10 @@ async def check_defaults_handling(
|
|||
if isinstance(value.default, DefaultValue) and not kwarg.endswith("_timeout")
|
||||
]
|
||||
|
||||
if method.__name__.endswith("_media_group"):
|
||||
# the parse_mode is applied to the first media item, and we test this elsewhere
|
||||
kwargs_need_default.remove("parse_mode")
|
||||
|
||||
defaults_no_custom_defaults = Defaults()
|
||||
kwargs = {kwarg: "custom_default" for kwarg in inspect.signature(Defaults).parameters.keys()}
|
||||
kwargs["tzinfo"] = pytz.timezone("America/New_York")
|
||||
|
@ -732,7 +736,7 @@ async def check_defaults_handling(
|
|||
data = request_data.parameters
|
||||
|
||||
# Check regular arguments that need defaults
|
||||
for arg in (dkw for dkw in kwargs_need_default if dkw != "timeout"):
|
||||
for arg in kwargs_need_default:
|
||||
# 'None' should not be passed along to Telegram
|
||||
if df_value in [None, DEFAULT_NONE]:
|
||||
if arg in data:
|
||||
|
|
|
@ -432,6 +432,27 @@ def media_group(photo, thumb): # noqa: F811
|
|||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="function") # noqa: F811
|
||||
def media_group_no_caption_args(photo, thumb): # noqa: F811
|
||||
return [InputMediaPhoto(photo), InputMediaPhoto(thumb), InputMediaPhoto(photo)]
|
||||
|
||||
|
||||
@pytest.fixture(scope="function") # noqa: F811
|
||||
def media_group_no_caption_only_caption_entities(photo, thumb): # noqa: F811
|
||||
return [
|
||||
InputMediaPhoto(photo, caption_entities=[MessageEntity(MessageEntity.BOLD, 0, 5)]),
|
||||
InputMediaPhoto(photo, caption_entities=[MessageEntity(MessageEntity.BOLD, 0, 5)]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="function") # noqa: F811
|
||||
def media_group_no_caption_only_parse_mode(photo, thumb): # noqa: F811
|
||||
return [
|
||||
InputMediaPhoto(photo, parse_mode="Markdown"),
|
||||
InputMediaPhoto(thumb, parse_mode="HTML"),
|
||||
]
|
||||
|
||||
|
||||
class TestSendMediaGroup:
|
||||
@flaky(3, 1)
|
||||
async def test_send_media_group_photo(self, bot, chat_id, media_group):
|
||||
|
@ -445,6 +466,79 @@ class TestSendMediaGroup:
|
|||
mes.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)] for mes in messages
|
||||
)
|
||||
|
||||
async def test_send_media_group_throws_error_with_group_caption_and_individual_captions(
|
||||
self,
|
||||
bot,
|
||||
chat_id,
|
||||
media_group,
|
||||
media_group_no_caption_only_caption_entities,
|
||||
media_group_no_caption_only_parse_mode,
|
||||
):
|
||||
for group in (
|
||||
media_group,
|
||||
media_group_no_caption_only_caption_entities,
|
||||
media_group_no_caption_only_parse_mode,
|
||||
):
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="You can only supply either group caption or media with captions.",
|
||||
):
|
||||
await bot.send_media_group(chat_id, group, caption="foo")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"caption, parse_mode, caption_entities",
|
||||
[
|
||||
# same combinations of caption options as in media_group fixture
|
||||
("*photo* 1", "Markdown", None),
|
||||
("<b>photo</b> 1", "HTML", None),
|
||||
("photo 1", None, [MessageEntity(MessageEntity.BOLD, 0, 5)]),
|
||||
],
|
||||
)
|
||||
@flaky(3, 1)
|
||||
async def test_send_media_group_with_group_caption(
|
||||
self,
|
||||
bot,
|
||||
chat_id,
|
||||
media_group_no_caption_args,
|
||||
caption,
|
||||
parse_mode,
|
||||
caption_entities,
|
||||
):
|
||||
# prepare a copy to check later on if calling the method has caused side effects
|
||||
copied_media_group = media_group_no_caption_args.copy()
|
||||
|
||||
messages = await bot.send_media_group(
|
||||
chat_id,
|
||||
media_group_no_caption_args,
|
||||
caption=caption,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
)
|
||||
|
||||
# Check that the method had no side effects:
|
||||
# original group was not changed and 1st item still points to the same object
|
||||
# (1st item must be copied within the method before adding the caption)
|
||||
assert media_group_no_caption_args == copied_media_group
|
||||
assert media_group_no_caption_args[0] is copied_media_group[0]
|
||||
|
||||
assert not any(item.parse_mode for item in media_group_no_caption_args)
|
||||
|
||||
assert isinstance(messages, list)
|
||||
assert len(messages) == 3
|
||||
assert all(isinstance(mes, Message) for mes in messages)
|
||||
|
||||
first_message, other_messages = messages[0], messages[1:]
|
||||
assert all(mes.media_group_id == first_message.media_group_id for mes in messages)
|
||||
|
||||
# Make sure first message got the caption, which will lead
|
||||
# to Telegram displaying its caption as group caption
|
||||
assert first_message.caption
|
||||
assert first_message.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)]
|
||||
|
||||
# Check that other messages have no captions
|
||||
assert all(mes.caption is None for mes in other_messages)
|
||||
assert not any(mes.caption_entities for mes in other_messages)
|
||||
|
||||
@flaky(3, 1)
|
||||
async def test_send_media_group_all_args(self, bot, raw_bot, chat_id, media_group):
|
||||
ext_bot = bot
|
||||
|
@ -600,6 +694,51 @@ class TestSendMediaGroup:
|
|||
)
|
||||
assert not all(msg.has_protected_content for msg in unprotected)
|
||||
|
||||
@flaky(3, 1)
|
||||
@pytest.mark.parametrize("default_bot", [{"parse_mode": ParseMode.HTML}], indirect=True)
|
||||
async def test_send_media_group_default_parse_mode(
|
||||
self, chat_id, media_group_no_caption_args, default_bot
|
||||
):
|
||||
default = await default_bot.send_media_group(
|
||||
chat_id, media_group_no_caption_args, caption="<b>photo</b> 1"
|
||||
)
|
||||
|
||||
# make sure no parse_mode was set as a side effect
|
||||
assert not any(item.parse_mode for item in media_group_no_caption_args)
|
||||
|
||||
overridden_markdown_v2 = await default_bot.send_media_group(
|
||||
chat_id,
|
||||
media_group_no_caption_args.copy(),
|
||||
caption="*photo* 1",
|
||||
parse_mode=ParseMode.MARKDOWN_V2,
|
||||
)
|
||||
|
||||
overridden_none = await default_bot.send_media_group(
|
||||
chat_id,
|
||||
media_group_no_caption_args.copy(),
|
||||
caption="<b>photo</b> 1",
|
||||
parse_mode=None,
|
||||
)
|
||||
|
||||
# Make sure first message got the caption, which will lead to Telegram
|
||||
# displaying its caption as group caption
|
||||
assert overridden_none[0].caption == "<b>photo</b> 1"
|
||||
assert not overridden_none[0].caption_entities
|
||||
# First messages in these two groups have to have caption "photo 1"
|
||||
# because of parse mode (default or explicit)
|
||||
for mes_group in (default, overridden_markdown_v2):
|
||||
first_message = mes_group[0]
|
||||
assert first_message.caption == "photo 1"
|
||||
assert first_message.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)]
|
||||
|
||||
# This check is valid for all 3 groups of messages
|
||||
for mes_group in (default, overridden_markdown_v2, overridden_none):
|
||||
first_message, other_messages = mes_group[0], mes_group[1:]
|
||||
assert all(mes.media_group_id == first_message.media_group_id for mes in mes_group)
|
||||
# Check that messages from 2nd message onwards have no captions
|
||||
assert all(mes.caption is None for mes in other_messages)
|
||||
assert not any(mes.caption_entities for mes in other_messages)
|
||||
|
||||
@flaky(3, 1)
|
||||
async def test_edit_message_media(self, bot, raw_bot, chat_id, media_group):
|
||||
ext_bot = bot
|
||||
|
|
|
@ -118,6 +118,9 @@ def check_method(h4):
|
|||
ignored |= {"venue"} # Added for ease of use
|
||||
elif name == "answerInlineQuery":
|
||||
ignored |= {"current_offset"} # Added for ease of use
|
||||
elif name == "sendMediaGroup":
|
||||
# Added for ease of use
|
||||
ignored |= {"caption", "parse_mode", "caption_entities"}
|
||||
|
||||
assert (sig.parameters.keys() ^ checked) - ignored == set()
|
||||
|
||||
|
|
Loading…
Reference in a new issue