8000 Api 7.3 InputPollOption by Bibo-Joshi · Pull Request #4248 · python-telegram-bot/python-telegram-bot · GitHub
[go: up one dir, main page]

Skip to content

Api 7.3 InputPollOption #4248

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 10, 2024
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
1 change: 1 addition & 0 deletions docs/source/telegram.at-tree.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Available Types
telegram.inputmediadocument
telegram.inputmediaphoto
telegram.inputmediavideo
telegram.inputpolloption
telegram.inputsticker
telegram.keyboardbutton
telegram.keyboardbuttonpolltype
Expand Down
6 changes: 6 additions & 0 deletions docs/source/telegram.inputpolloption.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
InputPollOption
===============

.. autoclass:: telegram.InputPollOption
:members:
:show-inheritance:
3 changes: 2 additions & 1 deletion telegram/__init__.py
10000
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"InputMediaPhoto",
"InputMediaVideo",
"InputMessageContent",
"InputPollOption",
"InputSticker",
"InputTextMessageContent",
"InputVenueMessageContent",
Expand Down Expand Up @@ -403,7 +404,7 @@
from ._payment.shippingoption import ShippingOption
from ._payment.shippingquery import ShippingQuery
from ._payment.successfulpayment import SuccessfulPayment
from ._poll import Poll, PollAnswer, PollOption
from ._poll import InputPollOption, Poll, PollAnswer, PollOption
from ._proximityalerttriggered import ProximityAlertTriggered
from ._reaction import ReactionCount, ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji
from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote
Expand Down
21 changes: 15 additions & 6 deletions telegram/_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
from telegram._menubutton import MenuButton
from telegram._message import Message
from telegram._messageid import MessageId
from telegram._poll import Poll
from telegram._poll import InputPollOption, Poll
from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji
from telegram._reply import ReplyParameters
from telegram._sentwebappmessage import SentWebAppMessage
Expand Down Expand Up @@ -6815,7 +6815,7 @@ async def send_poll(
self,
chat_id: Union[int, str],
question: str,
options: Sequence[str],
options: Sequence[Union[str, "InputPollOption"]],
is_anonymous: Optional[bool] = None,
type: Optional[str] = None, # pylint: disable=redefined-builtin
allows_multiple_answers: Optional[bool] = None,
Expand Down Expand Up @@ -6848,14 +6848,20 @@ async def send_poll(
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
question (:obj:`str`): Poll question, :tg-const:`telegram.Poll.MIN_QUESTION_LENGTH`-
:tg-const:`telegram.Poll.MAX_QUESTION_LENGTH` characters.
options (Sequence[:obj:`str`]): Sequence of answer options,
options (Sequence[:obj:`str` | :class:`telegram.InputPollOption`]): Sequence of
:tg-const:`telegram.Poll.MIN_OPTION_NUMBER`-
:tg-const:`telegram.Poll.MAX_OPTION_NUMBER` strings
:tg-const:`telegram.Poll.MAX_OPTION_NUMBER` answer options. Each option may either
be a string with
:tg-const:`telegram.Poll.MIN_OPTION_LENGTH`-
:tg-const:`telegram.Poll.MAX_OPTION_LENGTH` characters each.
:tg-const:`telegram.Poll.MAX_OPTION_LENGTH` characters or an
:class:`~telegram.InputPollOption` object. Strings are converted to
:class:`~telegram.InputPollOption` objects automatically.

.. versionchanged:: 20.0
|sequenceargs|

.. versionchanged:: NEXT.VERSION
Bot API 7.3 adds support for :class:`~telegram.InputPollOption` objects.
is_anonymous (:obj:`bool`, optional): :obj:`True`, if the poll needs to be anonymous,
defaults to :obj:`True`.
type (:obj:`str`, optional): Poll type, :tg-const:`telegram.Poll.QUIZ` or
Expand Down Expand Up @@ -6941,7 +6947,10 @@ async def send_poll(
data: JSONDict = {
"chat_id": chat_id,
"question": question,
"options": options,
"options": [
InputPollOption(option) if isinstance(option, str) else option
for option in options
],
"explanation_parse_mode": explanation_parse_mode,
"is_anonymous": is_anonymous,
"type": type,
Expand Down
3 changes: 2 additions & 1 deletion telegram/_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
InputMediaDocument,
InputMediaPhoto,
InputMediaVideo,
InputPollOption,
LabeledPrice,
LinkPreviewOptions,
Location,
Expand Down Expand Up @@ -2545,7 +2546,7 @@ async def send_voice(
async def send_poll(
self,
question: str,
options: Sequence[str],
options: Sequence[Union[str, "InputPollOption"]],
is_anonymous: Optional[bool] = None,
type: Optional[str] = None,
allows_multiple_answers: Optional[bool] = None,
Expand Down
3 changes: 2 additions & 1 deletion telegram/_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
InputMediaDocument,
InputMediaPhoto,
InputMediaVideo,
InputPollOption,
LabeledPrice,
MessageId,
MessageOrigin,
Expand Down Expand Up @@ -2890,7 +2891,7 @@ async def reply_contact(
async def reply_poll(
self,
question: str,
options: Sequence[str],
options: Sequence[Union[str, "InputPollOption"]],
is_anonymous: Optional[bool] = None,
type: Optional[str] = None, # pylint: disable=redefined-builtin
allows_multiple_answers: Optional[bool] = None,
Expand Down
69 changes: 68 additions & 1 deletion telegram/_poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,79 @@
from telegram._utils import enum
from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.types import JSONDict, ODVInput

if TYPE_CHECKING:
from telegram import Bot


class InputPollOption(TelegramObject):
"""
This object contains information about one answer option in a poll to send.

Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`text` is equal.

.. versionadded:: NEXT.VERSION

Args:
text (:obj:`str`): Option text,
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
characters.
text_parse_mode (:obj:`str`, optional): |parse_mode|
Currently, only custom emoji entities are allowed.
text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities
that appear in the option :paramref:`text`. It can be specified instead of
:paramref:`text_parse_mode`.
Currently, only custom emoji entities are allowed.
This list is empty if the text does not contain entities.

Attributes:
text (:obj:`str`): Option text,
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
characters.
text_parse_mode (:obj:`str`): Optional. |parse_mode|
Currently, only custom emoji entities are allowed.
text_entities (Sequence[:class:`telegram.MessageEntity`]): Special entities
that appear in the option :paramref:`text`. It can be specified instead of
:paramref:`text_parse_mode`.
Currently, only custom emoji entities are allowed.
This list is empty if the text does not contain entities.
"""

__slots__ = ("text", "text_entities", "text_parse_mode")

def __init__(
self,
text: str,
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Optional[Sequence[MessageEntity]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.text: str = text
self.text_parse_mode: ODVInput[str] = text_parse_mode
self.text_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)

self._id_attrs = (self.text,)

self._freeze()

@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InputPollOption"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)

if not data:
return None

data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot)

return super().de_json(data=data, bot=bot)


class PollOption(TelegramObject):
"""
This object contains information about one answer option in a poll.
Expand Down
3 changes: 2 additions & 1 deletion telegram/_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
InputMediaDocument,
InputMediaPhoto,
InputMediaVideo,
InputPollOption,
LabeledPrice,
LinkPreviewOptions,
Location,
Expand Down Expand Up @@ -1482,7 +1483,7 @@ async def send_voice(
async def send_poll(
self,
question: str,
options: Sequence[str],
options: Sequence[Union[str, "InputPollOption"]],
is_anonymous: Optional[bool] = None,
type: Optional[str] = None,
allows_multiple_answers: Optional[bool] = None,
Expand Down
15 changes: 15 additions & 0 deletions telegram/ext/_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,21 @@ def quote_parse_mode(self, value: object) -> NoReturn:
"You can not assign a new value to quote_parse_mode after initialization."
)

@property
def text_parse_mode(self) -> Optional[str]:
""":obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
the corresponding parameter of :meth:`telegram.InputPollOption`.

.. versionadded:: NEXT.VERSION
"""
return self._parse_mode

@text_parse_mode.setter
def text_parse_mode(self, value: object) -> NoReturn:
raise AttributeError(
"You can not assign a new value to text_parse_mode after initialization."
)

@property
def disable_notification(self) -> Optional[bool]:
""":obj:`bool`: Optional. Sends the message silently. Users will
Expand Down
18 changes: 17 additions & 1 deletion telegram/ext/_extbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
InlineKeyboardMarkup,
InlineQueryResultsButton,
InputMedia,
InputPollOption,
LinkPreviewOptions,
Location,
MaskPosition,
Expand Down Expand Up @@ -436,6 +437,7 @@ def _insert_defaults(self, data: Dict[str, object]) -> None:
# 3) set the correct parse_mode for all InputMedia objects
# 4) handle the LinkPreviewOptions case (see below)
# 5) handle the ReplyParameters case (see below)
# 6) handle text_ E377 parse_mode in InputPollOption
for key, val in data.items():
# 1)
if isinstance(val, DefaultValue):
Expand Down Expand Up @@ -487,6 +489,20 @@ def _insert_defaults(self, data: Dict[str, object]) -> None:

data[key] = new_value

elif isinstance(val, Sequence) and all(
isinstance(obj, InputPollOption) for obj in val
):
new_val = []
for option in val:
if not isinstance(option.text_parse_mode, DefaultValue):
new_val.append(option)
else:
new_option = copy(option)
with new_option._unfrozen():
new_option.text_parse_mode = self.defaults.text_parse_mode
new_val.append(new_option)
data[key] = new_val

def _replace_keyboard(self, reply_markup: Optional[KT]) -> Optional[KT]:
# If the reply_markup is an inline keyboard and we allow arbitrary callback data, let the
# CallbackDataCache build a new keyboard with the data replaced. Otherwise return the input
Expand Down Expand Up @@ -2917,7 +2933,7 @@ async def send_poll(
self,
chat_id: Union[int, str],
question: str,
options: Sequence[str],
options: Sequence[Union[str, "InputPollOption"]],
is_anonymous: Optional[bool] = None,
type: Optional[str] = None, # pylint: disable=redefined-builtin
allows_multiple_answers: Optional[bool] = None,
Expand Down
2 changes: 2 additions & 0 deletions tests/auxil/bot_method_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ def build_kwargs(
elif name == "ok":
kws["ok"] = False
kws["error_message"] = "error"
elif name == "options":
kws[name] = ["option1", "option2"]
else:
kws[name] = True

Expand Down
55 changes: 52 additions & 3 deletions tests/test_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
InputMediaDocument,
InputMediaPhoto,
InputMessageContent,
InputPollOption,
InputTextMessageContent,
LabeledPrice,
LinkPreviewOptions,
Expand Down Expand Up @@ -1937,6 +1938,54 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs):
chat_id, message, reply_parameters=ReplyParameters(**kwargs)
)

@pytest.mark.parametrize(
("default_bot", "custom"),
[
({"parse_mode": ParseMode.HTML}, "NOTHING"),
({"parse_mode": ParseMode.HTML}, None),
({"parse_mode": ParseMode.HTML}, ParseMode.MARKDOWN_V2),
({"parse_mode": None}, ParseMode.MARKDOWN_V2),
],
indirect=["default_bot"],
)
async def test_send_poll_default_text_parse_mode(
self, default_bot, raw_bot, chat_id, custom, monkeypatch
):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
option_1 = request_data.parameters["options"][0]
option_2 = request_data.parameters["options"][1]
assert option_1.get("text_parse_mode") == (default_bot.defaults.text_parse_mode)
assert option_2.get("text_parse_mode") == (
default_bot.defaults.text_parse_mode if custom == "NOTHING" else custom
)
return make_message("dummy reply").to_dict()

async def make_raw_assertion(url, request_data: RequestData, *args, **kwargs):
option_1 = request_data.parameters["options"][0]
option_2 = request_data.parameters["options"][1]
assert option_1.get("text_parse_mode") is None
assert option_2.get("text_parse_mode") == (None if custom == "NOTHING" else custom)
return make_message("dummy reply").to_dict()

if custom == "NOTHING":
option_2 = InputPollOption("option2")
else:
option_2 = InputPollOption("option2", text_parse_mode=custom)

monkeypatch.setattr(default_bot.request, "post", make_assertion)
await default_bot.send_poll(
chat_id,
question="question",
options=["option1", option_2],
)

monkeypatch.setattr(raw_bot.request, "post", make_raw_assertion)
await raw_bot.send_poll(
chat_id,
question="question",
options=["option1", option_2],
)

@pytest.mark.parametrize(
("default_bot", "custom"),
[
Expand Down Expand Up @@ -2326,7 +2375,7 @@ async def make_assertion(*args, **_):
)
async def test_send_and_stop_poll(self, bot, super_group_id, reply_markup):
question = "Is this a test?"
answers = ["Yes", "No", "Maybe"]
answers = ["Yes", InputPollOption("No"), "Maybe"]
explanation = "[Here is a link](https://google.com)"
explanation_entities = [
MessageEntity(MessageEntity.TEXT_LINK, 0, 14, url="https://google.com")
Expand Down Expand Up @@ -2360,7 +2409,7 @@ async def test_send_and_stop_poll(self, bot, super_group_id, reply_markup):
assert message.poll
assert message.poll.question == question
assert message.poll.options[0].text == answers[0]
assert message.poll.options[1].text == answers[1]
assert message.poll.options[1].text == answers[1].text
assert message.poll.options[2].text == answers[2]
assert not message.poll.is_anonymous
assert message.poll.allows_multiple_answers
Expand All @@ -2380,7 +2429,7 @@ async def test_send_and_stop_poll(self, bot, super_group_id, reply_markup):
assert poll.is_closed
assert poll.options[0].text == answers[0]
assert poll.options[0].voter_count == 0
assert poll.options[1].text == answers[1]
assert poll.options[1].text == answers[1].text
assert poll.options[1].voter_count == 0
assert poll.options[2].text == answers[2]
assert poll.options[2].voter_count == 0
Expand Down
Loading
Loading
0