Handle Lists and Tuples and Datetimes Directly in TelegramObject.to_dict (#3353)

This commit is contained in:
Harshil 2022-11-14 01:58:41 +05:30 committed by GitHub
parent e1d56178c8
commit e54c6a04de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 65 additions and 254 deletions

View file

@ -3665,7 +3665,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
if inline_message_id:
data["inline_message_id"] = inline_message_id
if entities:
data["entities"] = [me.to_dict() for me in entities]
data["entities"] = [me.to_dict(recursive=True) for me in entities]
return await self._send_message(
"editMessageText",
@ -5085,7 +5085,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
data: JSONDict = {"shipping_query_id": shipping_query_id, "ok": ok}
if shipping_options is not None:
data["shipping_options"] = [option.to_dict() for option in shipping_options]
data["shipping_options"] = [option.to_dict(True) for option in shipping_options]
if error_message is not None:
data["error_message"] = error_message

View file

@ -22,7 +22,7 @@ from typing import TYPE_CHECKING, Optional
from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils.datetime import from_timestamp, to_timestamp
from telegram._utils.datetime import from_timestamp
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@ -150,11 +150,3 @@ class ChatInviteLink(TelegramObject):
data["expire_date"] = from_timestamp(data.get("expire_date", None))
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["expire_date"] = to_timestamp(self.expire_date)
return data

View file

@ -24,7 +24,7 @@ from telegram._chat import Chat
from telegram._chatinvitelink import ChatInviteLink
from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils.datetime import from_timestamp, to_timestamp
from telegram._utils.datetime import from_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.types import JSONDict, ODVInput
@ -103,14 +103,6 @@ class ChatJoinRequest(TelegramObject):
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["date"] = to_timestamp(self.date)
return data
async def approve(
self,
*,

View file

@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, ClassVar, Dict, Optional, Type
from telegram import constants
from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils.datetime import from_timestamp, to_timestamp
from telegram._utils.datetime import from_timestamp
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@ -123,15 +123,6 @@ class ChatMember(TelegramObject):
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
if data.get("until_date", False):
data["until_date"] = to_timestamp(data["until_date"])
return data
class ChatMemberOwner(ChatMember):
"""

View file

@ -25,7 +25,7 @@ from telegram._chatinvitelink import ChatInviteLink
from telegram._chatmember import ChatMember
from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils.datetime import from_timestamp, to_timestamp
from telegram._utils.datetime import from_timestamp
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@ -122,15 +122,6 @@ class ChatMemberUpdated(TelegramObject):
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
# Required
data["date"] = to_timestamp(self.date)
return data
def _get_attribute_difference(self, attribute: str) -> Tuple[object, object]:
try:
old = self.old_chat_member[attribute]

View file

@ -90,15 +90,6 @@ class InputMedia(TelegramObject):
self.caption_entities = caption_entities
self.parse_mode = parse_mode
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
if self.caption_entities:
data["caption_entities"] = [ce.to_dict() for ce in self.caption_entities]
return data
@staticmethod
def _parse_thumb_input(thumb: Optional[FileInput]) -> Optional[Union[str, InputFile]]:
# We use local_mode=True because we don't have access to the actual setting and want

View file

@ -282,14 +282,6 @@ class StickerSet(TelegramObject):
return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["stickers"] = [s.to_dict() for s in data.get("stickers")] # type: ignore[union-attr]
return data
class MaskPosition(TelegramObject):
"""This object describes the position on faces where a mask should be placed by default.

View file

@ -117,16 +117,6 @@ class Game(TelegramObject):
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["photo"] = [p.to_dict() for p in self.photo]
if self.text_entities:
data["text_entities"] = [x.to_dict() for x in self.text_entities]
return data
def parse_text_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.

View file

@ -68,16 +68,6 @@ class InlineKeyboardMarkup(TelegramObject):
self._id_attrs = (self.inline_keyboard,)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["inline_keyboard"] = []
for inline_keyboard in self.inline_keyboard:
data["inline_keyboard"].append([x.to_dict() for x in inline_keyboard])
return data
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InlineKeyboardMarkup"]:
"""See :meth:`telegram.TelegramObject.de_json`."""

View file

@ -53,18 +53,3 @@ class InlineQueryResult(TelegramObject):
self.id = str(id) # pylint: disable=invalid-name
self._id_attrs = (self.id,)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
# pylint: disable=no-member
if (
hasattr(self, "caption_entities")
and self.caption_entities # type: ignore[attr-defined]
):
data["caption_entities"] = [
ce.to_dict() for ce in self.caption_entities # type: ignore[attr-defined]
]
return data

View file

@ -228,14 +228,6 @@ class InputInvoiceMessageContent(InputMessageContent):
)
)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["prices"] = [price.to_dict() for price in self.prices]
return data
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: "Bot"

View file

@ -83,12 +83,3 @@ class InputTextMessageContent(InputMessageContent):
self.disable_web_page_preview = disable_web_page_preview
self._id_attrs = (self.message_text,)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
if self.entities:
data["entities"] = [ce.to_dict() for ce in self.entities]
return data

View file

@ -161,12 +161,6 @@ class MenuButtonWebApp(MenuButton):
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["web_app"] = self.web_app.to_dict()
return data
class MenuButtonDefault(MenuButton):
"""Describes that no specific value for the menu button was set.

View file

@ -47,7 +47,7 @@ from telegram._poll import Poll
from telegram._proximityalerttriggered import ProximityAlertTriggered
from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils.datetime import from_timestamp, to_timestamp
from telegram._utils.datetime import from_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
from telegram._utils.types import DVInput, FileInput, JSONDict, ODVInput, ReplyMarkup
from telegram._videochat import (
@ -719,30 +719,6 @@ class Message(TelegramObject):
return self._effective_attachment # type: ignore[return-value]
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
# Required
data["date"] = to_timestamp(self.date)
# Optionals
if self.forward_date:
data["forward_date"] = to_timestamp(self.forward_date)
if self.edit_date:
data["edit_date"] = to_timestamp(self.edit_date)
if self.photo:
data["photo"] = [p.to_dict() for p in self.photo]
if self.entities:
data["entities"] = [e.to_dict() for e in self.entities]
if self.caption_entities:
data["caption_entities"] = [e.to_dict() for e in self.caption_entities]
if self.new_chat_photo:
data["new_chat_photo"] = [p.to_dict() for p in self.new_chat_photo]
if self.new_chat_members:
data["new_chat_members"] = [u.to_dict() for u in self.new_chat_members]
return data
def _quote(self, quote: Optional[bool], reply_to_message_id: Optional[int]) -> Optional[int]:
"""Modify kwargs for replying with or without quoting."""
if reply_to_message_id is not None:

View file

@ -404,15 +404,6 @@ class SecureValue(TelegramObject):
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["files"] = [p.to_dict() for p in self.files] # type: ignore[union-attr]
data["translation"] = [p.to_dict() for p in self.translation] # type: ignore[union-attr]
return data
class _CredentialsBase(TelegramObject):
"""Base class for DataCredentials and FileCredentials."""
@ -450,15 +441,6 @@ class DataCredentials(_CredentialsBase):
def __init__(self, data_hash: str, secret: str, *, api_kwargs: JSONDict = None):
super().__init__(hash=data_hash, secret=secret, api_kwargs=api_kwargs)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
del data["file_hash"]
del data["hash"]
return data
class FileCredentials(_CredentialsBase):
"""
@ -478,12 +460,3 @@ class FileCredentials(_CredentialsBase):
def __init__(self, file_hash: str, secret: str, *, api_kwargs: JSONDict = None):
super().__init__(hash=file_hash, secret=secret, api_kwargs=api_kwargs)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
del data["data_hash"]
del data["hash"]
return data

View file

@ -244,14 +244,3 @@ class EncryptedPassportElement(TelegramObject):
)
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
if self.files:
data["files"] = [p.to_dict() for p in self.files]
if self.translation:
data["translation"] = [p.to_dict() for p in self.translation]
return data

View file

@ -81,14 +81,6 @@ class PassportData(TelegramObject):
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["data"] = [e.to_dict() for e in self.data]
return data
@property
def decrypted_data(self) -> List[EncryptedPassportElement]:
"""

View file

@ -64,11 +64,3 @@ class ShippingOption(TelegramObject):
self.prices = prices
self._id_attrs = (self.id,)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["prices"] = [p.to_dict() for p in self.prices]
return data

View file

@ -27,7 +27,7 @@ from telegram._messageentity import MessageEntity
from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils import enum
from telegram._utils.datetime import from_timestamp, to_timestamp
from telegram._utils.datetime import from_timestamp
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@ -228,17 +228,6 @@ class Poll(TelegramObject):
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["options"] = [x.to_dict() for x in self.options]
if self.explanation_entities:
data["explanation_entities"] = [e.to_dict() for e in self.explanation_entities]
data["close_date"] = to_timestamp(data.get("close_date"))
return data
def parse_explanation_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.

View file

@ -119,15 +119,6 @@ class ReplyKeyboardMarkup(TelegramObject):
self._id_attrs = (self.keyboard,)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["keyboard"] = []
for row in self.keyboard:
data["keyboard"].append([button.to_dict() for button in row])
return data
@classmethod
def from_button(
cls,

View file

@ -17,12 +17,14 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Base class for Telegram Objects."""
import datetime
import inspect
import json
from copy import deepcopy
from itertools import chain
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Sized, Tuple, Type, TypeVar, Union
from telegram._utils.datetime import to_timestamp
from telegram._utils.types import JSONDict
from telegram._utils.warnings import warn
@ -327,6 +329,32 @@ class TelegramObject:
:obj:`dict`
"""
out = self._get_attrs(recursive=recursive)
# Now we should convert TGObjects to dicts inside objects such as sequences, and convert
# datetimes to timestamps. This mostly eliminates the need for subclasses to override
# `to_dict`
for key, value in out.items():
if isinstance(value, (tuple, list)) and value:
val = [] # empty list to append our converted values to
for item in value:
if hasattr(item, "to_dict"):
val.append(item.to_dict(recursive=recursive))
# This branch is useful for e.g. List[List[PhotoSize|KeyboardButton]]
elif isinstance(item, (tuple, list)):
val.append(
[
i.to_dict(recursive=recursive) if hasattr(i, "to_dict") else i
for i in item
]
)
else: # if it's not a TGObject, just append it. E.g. [TGObject, 2]
val.append(item)
out[key] = val
elif isinstance(value, datetime.datetime):
out[key] = to_timestamp(value)
# Effectively "unpack" api_kwargs into `out`:
out.update(out.pop("api_kwargs", {})) # type: ignore[call-overload]
return out

View file

@ -69,15 +69,5 @@ class UserProfilePhotos(TelegramObject):
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
data["photos"] = []
for photo in self.photos:
data["photos"].append([x.to_dict() for x in photo])
return data
def __hash__(self) -> int:
return hash(tuple(tuple(p for p in photo) for photo in self.photos))

View file

@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, List, Optional
from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils.datetime import from_timestamp, to_timestamp
from telegram._utils.datetime import from_timestamp
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@ -121,14 +121,6 @@ class VideoChatParticipantsInvited(TelegramObject):
data["users"] = User.de_list(data.get("users", []), bot)
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
if self.users is not None:
data["users"] = [u.to_dict() for u in self.users]
return data
def __hash__(self) -> int:
return hash(None) if self.users is None else hash(tuple(self.users))
@ -175,12 +167,3 @@ class VideoChatScheduled(TelegramObject):
data["start_date"] = from_timestamp(data["start_date"])
return super().de_json(data=data, bot=bot)
def to_dict(self, recursive: bool = True) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict(recursive=recursive)
# Required
data["start_date"] = to_timestamp(self.start_date)
return data

View file

@ -1714,7 +1714,7 @@ class TestBot:
message=Message(
1,
from_user=User(1, "", False),
date=None,
date=dtm.datetime.utcnow(),
chat=Chat(1, ""),
text="Webhook",
),
@ -2997,7 +2997,10 @@ class TestBot:
)
message = Message(
1, None, None, reply_markup=bot.callback_data_cache.process_keyboard(reply_markup)
1,
dtm.datetime.utcnow(),
None,
reply_markup=bot.callback_data_cache.process_keyboard(reply_markup),
)
# We do to_dict -> de_json to make sure those aren't the same objects
message.pinned_message = Message.de_json(message.to_dict(), bot)
@ -3008,7 +3011,7 @@ class TestBot:
**{
message_type: Message(
1,
None,
dtm.datetime.utcnow(),
None,
pinned_message=message,
reply_to_message=Message.de_json(message.to_dict(), bot),
@ -3075,7 +3078,7 @@ class TestBot:
reply_markup = bot.callback_data_cache.process_keyboard(reply_markup)
message = Message(
1,
None,
dtm.datetime.utcnow(),
None,
reply_markup=reply_markup,
via_bot=bot.bot if self_sender else User(1, "first", False),

View file

@ -17,6 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
from datetime import datetime
import pytest
from telegram import Audio, Bot, CallbackQuery, Chat, Message, User
@ -45,7 +47,7 @@ class TestCallbackQuery:
id_ = "id"
from_user = User(1, "test_user", False)
chat_instance = "chat_instance"
message = Message(3, None, Chat(4, "private"), from_user=User(5, "bot", False))
message = Message(3, datetime.utcnow(), Chat(4, "private"), from_user=User(5, "bot", False))
data = "data"
inline_message_id = "inline_message_id"
game_short_name = "the_game"

View file

@ -47,7 +47,7 @@ def invite_link(creator):
class TestChatInviteLink:
link = "thisialink"
creates_join_request = (False,)
creates_join_request = False
primary = True
revoked = False
expire_date = datetime.datetime.now(datetime.timezone.utc)

View file

@ -223,6 +223,9 @@ class TestChatMemberTypes:
assert chat_member_dict["status"] == chat_member_type.status
assert chat_member_dict["user"] == chat_member_type.user.to_dict()
for slot in chat_member_type.__slots__: # additional verification for the optional args
assert getattr(chat_member_type, slot) == chat_member_dict[slot]
def test_equality(self, chat_member_type):
a = ChatMember(status="status", user=CMDefaults.user)
b = ChatMember(status="status", user=CMDefaults.user)

View file

@ -80,7 +80,11 @@ def message(bot):
"forward_from_message_id": 101,
"forward_date": datetime.utcnow(),
},
{"reply_to_message": Message(50, None, None, None)},
{
"reply_to_message": Message(
50, datetime.utcnow(), Chat(13, "channel"), User(9, "i", False)
)
},
{"edit_date": datetime.utcnow()},
{
"text": "a text message",
@ -124,7 +128,11 @@ def message(bot):
{"message_auto_delete_timer_changed": MessageAutoDeleteTimerChanged(42)},
{"migrate_to_chat_id": -12345},
{"migrate_from_chat_id": -54321},
{"pinned_message": Message(7, None, None, None)},
{
"pinned_message": Message(
7, datetime.utcnow(), Chat(13, "channel"), User(9, "i", False)
)
},
{"invoice": Invoice("my invoice", "invoice", "start", "EUR", 243)},
{
"successful_payment": SuccessfulPayment(

View file

@ -238,9 +238,9 @@ class TestTelegramObject:
with pytest.raises(RuntimeError):
unpickled.get_bot() # There should be no bot when we pickle TGObjects
assert unpickled.chat == chat
assert unpickled.chat == chat, f"{unpickled.chat._id_attrs} != {chat._id_attrs}"
assert unpickled.from_user == user
assert unpickled.date == date
assert unpickled.date == date, f"{unpickled.date} != {date}"
assert unpickled.photo[0] == photo
def test_pickle_apply_api_kwargs(self, bot):

View file

@ -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 time
from datetime import datetime
import pytest
@ -39,7 +40,7 @@ from telegram import (
)
from telegram._utils.datetime import from_timestamp
message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text")
message = Message(1, datetime.utcnow(), Chat(1, ""), from_user=User(1, "", False), text="Text")
chat_member_updated = ChatMemberUpdated(
Chat(1, "chat"),
User(1, "", False),