From 7b116be34442c40f263ed4a7b3629ce8df54913a Mon Sep 17 00:00:00 2001 From: Luca Bellanti Date: Tue, 18 Apr 2023 16:16:23 +0200 Subject: [PATCH] Localize Received `datetime` Objects According to `Defaults.tzinfo` (#3632) --- AUTHORS.rst | 1 + docs/substitutions/global.rst | 2 ++ telegram/_chatinvitelink.py | 13 +++++++++-- telegram/_chatjoinrequest.py | 13 +++++++++-- telegram/_chatmember.py | 19 +++++++++++++-- telegram/_chatmemberupdated.py | 13 +++++++++-- telegram/_message.py | 29 +++++++++++++++++++---- telegram/_poll.py | 13 +++++++++-- telegram/_utils/datetime.py | 29 ++++++++++++++++++----- telegram/_videochat.py | 13 +++++++++-- telegram/_webhookinfo.py | 39 ++++++++++++++++++++++--------- tests/_utils/test_datetime.py | 7 +++++- tests/conftest.py | 2 +- tests/test_chatinvitelink.py | 29 ++++++++++++++++++++++- tests/test_chatjoinrequest.py | 20 ++++++++++++++++ tests/test_chatmember.py | 20 +++++++++++++++- tests/test_chatmemberupdated.py | 26 +++++++++++++++++++++ tests/test_message.py | 41 +++++++++++++++++++++++++++++++++ tests/test_poll.py | 32 ++++++++++++++++++++++++- tests/test_videochat.py | 19 ++++++++++++++- tests/test_webhookinfo.py | 36 ++++++++++++++++++++++++++++- 21 files changed, 376 insertions(+), 40 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index c3aa909cc..b3a87a6c3 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -72,6 +72,7 @@ The following wonderful people contributed directly or indirectly to this projec - `Li-aung Yip `_ - `Loo Zheng Yuan `_ - `LRezende `_ +- `Luca Bellanti `_ - `macrojames `_ - `Matheus Lemos `_ - `Michael Dix `_ diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index d62c2f969..4d2160833 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -55,3 +55,5 @@ .. |sequenceargs| replace:: Accepts any :class:`collections.abc.Sequence` as input instead of just a list. .. |captionentitiesattr| replace:: Tuple of special entities that appear in the caption, which can be specified instead of ``parse_mode``. + +.. |datetime_localization| replace:: The default timezone of the bot is used for localization, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used. diff --git a/telegram/_chatinvitelink.py b/telegram/_chatinvitelink.py index a4611a2c3..058b0a5ff 100644 --- a/telegram/_chatinvitelink.py +++ b/telegram/_chatinvitelink.py @@ -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 +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -54,6 +54,9 @@ class ChatInviteLink(TelegramObject): is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked. expire_date (:class:`datetime.datetime`, optional): Date when the link will expire or has been expired. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| member_limit (:obj:`int`, optional): Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; :tg-const:`telegram.constants.ChatInviteLinkLimit.MIN_MEMBER_LIMIT`- @@ -78,6 +81,9 @@ class ChatInviteLink(TelegramObject): is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked. expire_date (:class:`datetime.datetime`): Optional. Date when the link will expire or has been expired. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| member_limit (:obj:`int`): Optional. Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; :tg-const:`telegram.constants.ChatInviteLinkLimit.MIN_MEMBER_LIMIT`- @@ -152,7 +158,10 @@ class ChatInviteLink(TelegramObject): if not data: return None + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["creator"] = User.de_json(data.get("creator"), bot) - data["expire_date"] = from_timestamp(data.get("expire_date", None)) + data["expire_date"] = from_timestamp(data.get("expire_date", None), tzinfo=loc_tzinfo) return super().de_json(data=data, bot=bot) diff --git a/telegram/_chatjoinrequest.py b/telegram/_chatjoinrequest.py index 610d9ff54..4d349ea70 100644 --- a/telegram/_chatjoinrequest.py +++ b/telegram/_chatjoinrequest.py @@ -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 +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput @@ -56,6 +56,9 @@ class ChatJoinRequest(TelegramObject): chat (:class:`telegram.Chat`): Chat to which the request was sent. from_user (:class:`telegram.User`): User that sent the join request. date (:class:`datetime.datetime`): Date the request was sent. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| user_chat_id (:obj:`int`): Identifier of a private chat with the user who sent the join request. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 @@ -73,6 +76,9 @@ class ChatJoinRequest(TelegramObject): chat (:class:`telegram.Chat`): Chat to which the request was sent. from_user (:class:`telegram.User`): User that sent the join request. date (:class:`datetime.datetime`): Date the request was sent. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| user_chat_id (:obj:`int`): Identifier of a private chat with the user who sent the join request. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 @@ -124,9 +130,12 @@ class ChatJoinRequest(TelegramObject): if not data: return None + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["chat"] = Chat.de_json(data.get("chat"), bot) data["from_user"] = User.de_json(data.pop("from", None), bot) - data["date"] = from_timestamp(data.get("date", None)) + data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo) data["invite_link"] = ChatInviteLink.de_json(data.get("invite_link"), bot) return super().de_json(data=data, bot=bot) diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index bdcdd8663..b73cc7896 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -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 +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -125,7 +125,10 @@ class ChatMember(TelegramObject): data["user"] = User.de_json(data.get("user"), bot) if "until_date" in data: - data["until_date"] = from_timestamp(data["until_date"]) + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + + data["until_date"] = from_timestamp(data["until_date"], tzinfo=loc_tzinfo) return super().de_json(data=data, bot=bot) @@ -386,6 +389,9 @@ class ChatMemberRestricted(ChatMember): .. versionadded:: 20.0 until_date (:class:`datetime.datetime`): Date when restrictions will be lifted for this user. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| can_send_audios (:obj:`bool`): :obj:`True`, if the user is allowed to send audios. .. versionadded:: 20.1 @@ -438,6 +444,9 @@ class ChatMemberRestricted(ChatMember): .. versionadded:: 20.0 until_date (:class:`datetime.datetime`): Date when restrictions will be lifted for this user. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| can_send_audios (:obj:`bool`): :obj:`True`, if the user is allowed to send audios. .. versionadded:: 20.1 @@ -565,6 +574,9 @@ class ChatMemberBanned(ChatMember): until_date (:class:`datetime.datetime`): Date when restrictions will be lifted for this user. + .. versionchanged:: NEXT.VERSION + |datetime_localization| + Attributes: status (:obj:`str`): The member's status in the chat, always :tg-const:`telegram.ChatMember.BANNED`. @@ -572,6 +584,9 @@ class ChatMemberBanned(ChatMember): until_date (:class:`datetime.datetime`): Date when restrictions will be lifted for this user. + .. versionchanged:: NEXT.VERSION + |datetime_localization| + """ __slots__ = ("until_date",) diff --git a/telegram/_chatmemberupdated.py b/telegram/_chatmemberupdated.py index 78f6c18cb..650d7d0b0 100644 --- a/telegram/_chatmemberupdated.py +++ b/telegram/_chatmemberupdated.py @@ -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 +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -52,6 +52,9 @@ class ChatMemberUpdated(TelegramObject): from_user (:class:`telegram.User`): Performer of the action, which resulted in the change. date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to :class:`datetime.datetime`. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member. new_chat_member (:class:`telegram.ChatMember`): New information about the chat member. invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link, which was used @@ -62,6 +65,9 @@ class ChatMemberUpdated(TelegramObject): from_user (:class:`telegram.User`): Performer of the action, which resulted in the change. date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to :class:`datetime.datetime`. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member. new_chat_member (:class:`telegram.ChatMember`): New information about the chat member. invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link, which was used @@ -118,9 +124,12 @@ class ChatMemberUpdated(TelegramObject): if not data: return None + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["chat"] = Chat.de_json(data.get("chat"), bot) data["from_user"] = User.de_json(data.pop("from", None), bot) - data["date"] = from_timestamp(data.get("date")) + data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo) data["old_chat_member"] = ChatMember.de_json(data.get("old_chat_member"), bot) data["new_chat_member"] = ChatMember.de_json(data.get("new_chat_member"), bot) data["invite_link"] = ChatInviteLink.de_json(data.get("invite_link"), bot) diff --git a/telegram/_message.py b/telegram/_message.py index f870e5077..8a962f74b 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -56,7 +56,7 @@ from telegram._shared import ChatShared, UserShared from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.datetime import from_timestamp +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram._utils.types import DVInput, FileInput, JSONDict, ODVInput, ReplyMarkup from telegram._videochat import ( @@ -121,6 +121,9 @@ class Message(TelegramObject): sent on behalf of a chat. date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to :class:`datetime.datetime`. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| chat (:class:`telegram.Chat`): Conversation the message belongs to. forward_from (:class:`telegram.User`, optional): For forwarded messages, sender of the original message. @@ -132,6 +135,9 @@ class Message(TelegramObject): users who disallow adding a link to their account in forwarded messages. forward_date (:class:`datetime.datetime`, optional): For forwarded messages, date the original message was sent in Unix time. Converted to :class:`datetime.datetime`. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| is_automatic_forward (:obj:`bool`, optional): :obj:`True`, if the message is a channel post that was automatically forwarded to the connected discussion group. @@ -141,6 +147,9 @@ class Message(TelegramObject): ``reply_to_message`` fields even if it itself is a reply. edit_date (:class:`datetime.datetime`, optional): Date the message was last edited in Unix time. Converted to :class:`datetime.datetime`. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| has_protected_content (:obj:`bool`, optional): :obj:`True`, if the message can't be forwarded. @@ -338,6 +347,9 @@ class Message(TelegramObject): sent on behalf of a chat. date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to :class:`datetime.datetime`. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| chat (:class:`telegram.Chat`): Conversation the message belongs to. forward_from (:class:`telegram.User`): Optional. For forwarded messages, sender of the original message. @@ -347,6 +359,9 @@ class Message(TelegramObject): the original message in the channel. forward_date (:class:`datetime.datetime`): Optional. For forwarded messages, date the original message was sent in Unix time. Converted to :class:`datetime.datetime`. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| is_automatic_forward (:obj:`bool`): Optional. :obj:`True`, if the message is a channel post that was automatically forwarded to the connected discussion group. @@ -356,6 +371,9 @@ class Message(TelegramObject): ``reply_to_message`` fields even if it itself is a reply. edit_date (:class:`datetime.datetime`): Optional. Date the message was last edited in Unix time. Converted to :class:`datetime.datetime`. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| has_protected_content (:obj:`bool`): Optional. :obj:`True`, if the message can't be forwarded. @@ -850,17 +868,20 @@ class Message(TelegramObject): if not data: return None + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["from_user"] = User.de_json(data.pop("from", None), bot) data["sender_chat"] = Chat.de_json(data.get("sender_chat"), bot) - data["date"] = from_timestamp(data["date"]) + data["date"] = from_timestamp(data["date"], tzinfo=loc_tzinfo) data["chat"] = Chat.de_json(data.get("chat"), bot) data["entities"] = MessageEntity.de_list(data.get("entities"), bot) data["caption_entities"] = MessageEntity.de_list(data.get("caption_entities"), bot) data["forward_from"] = User.de_json(data.get("forward_from"), bot) data["forward_from_chat"] = Chat.de_json(data.get("forward_from_chat"), bot) - data["forward_date"] = from_timestamp(data.get("forward_date")) + data["forward_date"] = from_timestamp(data.get("forward_date"), tzinfo=loc_tzinfo) data["reply_to_message"] = Message.de_json(data.get("reply_to_message"), bot) - data["edit_date"] = from_timestamp(data.get("edit_date")) + data["edit_date"] = from_timestamp(data.get("edit_date"), tzinfo=loc_tzinfo) data["audio"] = Audio.de_json(data.get("audio"), bot) data["document"] = Document.de_json(data.get("document"), bot) data["animation"] = Animation.de_json(data.get("animation"), bot) diff --git a/telegram/_poll.py b/telegram/_poll.py index 2182d3146..0e1d56c62 100644 --- a/telegram/_poll.py +++ b/telegram/_poll.py @@ -26,7 +26,7 @@ from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.datetime import from_timestamp +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -173,6 +173,9 @@ class Poll(TelegramObject): close_date (:obj:`datetime.datetime`, optional): Point in time (Unix timestamp) when the poll will be automatically closed. Converted to :obj:`datetime.datetime`. + .. versionchanged:: NEXT.VERSION + |datetime_localization| + Attributes: id (:obj:`str`): Unique poll identifier. question (:obj:`str`): Poll question, :tg-const:`telegram.Poll.MIN_QUESTION_LENGTH`- @@ -206,6 +209,9 @@ class Poll(TelegramObject): close_date (:obj:`datetime.datetime`): Optional. Point in time when the poll will be automatically closed. + .. versionchanged:: NEXT.VERSION + |datetime_localization| + """ __slots__ = ( @@ -271,9 +277,12 @@ class Poll(TelegramObject): if not data: return None + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["options"] = [PollOption.de_json(option, bot) for option in data["options"]] data["explanation_entities"] = MessageEntity.de_list(data.get("explanation_entities"), bot) - data["close_date"] = from_timestamp(data.get("close_date")) + data["close_date"] = from_timestamp(data.get("close_date"), tzinfo=loc_tzinfo) return super().de_json(data=data, bot=bot) diff --git a/telegram/_utils/datetime.py b/telegram/_utils/datetime.py index 7b647214e..c513f73c8 100644 --- a/telegram/_utils/datetime.py +++ b/telegram/_utils/datetime.py @@ -29,7 +29,10 @@ Warning: """ import datetime as dtm # skipcq: PYL-W0406 import time -from typing import Optional, Union +from typing import TYPE_CHECKING, Optional, Union + +if TYPE_CHECKING: + from telegram import Bot # pytz is only available if it was installed as dependency of APScheduler, so we make a little # workaround here @@ -162,7 +165,10 @@ def to_timestamp( ) -def from_timestamp(unixtime: Optional[int], tzinfo: dtm.tzinfo = UTC) -> Optional[dtm.datetime]: +def from_timestamp( + unixtime: Optional[int], + tzinfo: Optional[dtm.tzinfo] = None, +) -> Optional[dtm.datetime]: """ Converts an (integer) unix timestamp to a timezone aware datetime object. :obj:`None` s are left alone (i.e. ``from_timestamp(None)`` is :obj:`None`). @@ -170,7 +176,8 @@ def from_timestamp(unixtime: Optional[int], tzinfo: dtm.tzinfo = UTC) -> Optiona Args: unixtime (:obj:`int`): Integer POSIX timestamp. tzinfo (:obj:`datetime.tzinfo`, optional): The timezone to which the timestamp is to be - converted to. Defaults to UTC. + converted to. Defaults to :obj:`None`, in which case the returned datetime object will + be timezone aware and in UTC. Returns: Timezone aware equivalent :obj:`datetime.datetime` value if :paramref:`unixtime` is not @@ -179,9 +186,19 @@ def from_timestamp(unixtime: Optional[int], tzinfo: dtm.tzinfo = UTC) -> Optiona if unixtime is None: return None - if tzinfo is not None: - return dtm.datetime.fromtimestamp(unixtime, tz=tzinfo) - return dtm.datetime.utcfromtimestamp(unixtime) + return dtm.datetime.fromtimestamp(unixtime, tz=UTC if tzinfo is None else tzinfo) + + +def extract_tzinfo_from_defaults(bot: "Bot") -> Union[dtm.tzinfo, None]: + """ + Extracts the timezone info from the default values of the bot. + If the bot has no default values, :obj:`None` is returned. + """ + # We don't use `ininstance(bot, ExtBot)` here so that this works + # in `python-telegram-bot-raw` as well + if hasattr(bot, "defaults") and bot.defaults: + return bot.defaults.tzinfo + return None def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float: diff --git a/telegram/_videochat.py b/telegram/_videochat.py index a025809e5..d7b348b0e 100644 --- a/telegram/_videochat.py +++ b/telegram/_videochat.py @@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Optional, Sequence, Tuple from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.datetime import from_timestamp +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -149,10 +149,16 @@ class VideoChatScheduled(TelegramObject): Args: start_date (:obj:`datetime.datetime`): Point in time (Unix timestamp) when the video chat is supposed to be started by a chat administrator + + .. versionchanged:: NEXT.VERSION + |datetime_localization| Attributes: start_date (:obj:`datetime.datetime`): Point in time (Unix timestamp) when the video chat is supposed to be started by a chat administrator + .. versionchanged:: NEXT.VERSION + |datetime_localization| + """ __slots__ = ("start_date",) @@ -178,6 +184,9 @@ class VideoChatScheduled(TelegramObject): if not data: return None - data["start_date"] = from_timestamp(data["start_date"]) + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + + data["start_date"] = from_timestamp(data["start_date"], tzinfo=loc_tzinfo) return super().de_json(data=data, bot=bot) diff --git a/telegram/_webhookinfo.py b/telegram/_webhookinfo.py index 2b444c410..c62935a7d 100644 --- a/telegram/_webhookinfo.py +++ b/telegram/_webhookinfo.py @@ -21,7 +21,7 @@ from typing import TYPE_CHECKING, Optional, Sequence, Tuple from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.datetime import from_timestamp +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -49,8 +49,11 @@ class WebhookInfo(TelegramObject): webhook certificate checks. pending_update_count (:obj:`int`): Number of updates awaiting delivery. ip_address (:obj:`str`, optional): Currently used webhook IP address. - last_error_date (:obj:`int`, optional): Unix time for the most recent error that happened - when trying to deliver an update via webhook. + last_error_date (:class:`datetime.datetime`): Optional. Datetime for the most recent + error that happened when trying to deliver an update via webhook. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| last_error_message (:obj:`str`, optional): Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook. max_connections (:obj:`int`, optional): Maximum allowed number of simultaneous HTTPS @@ -62,18 +65,25 @@ class WebhookInfo(TelegramObject): .. versionchanged:: 20.0 |sequenceclassargs| - last_synchronization_error_date (:obj:`int`, optional): Unix time of the most recent error - that happened when trying to synchronize available updates with Telegram datacenters. + last_synchronization_error_date (:class:`datetime.datetime`, optional): Datetime of the + most recent error that happened when trying to synchronize available updates with + Telegram datacenters. .. versionadded:: 20.0 + + .. versionchanged:: NEXT.VERSION + |datetime_localization| Attributes: url (:obj:`str`): Webhook URL, may be empty if webhook is not set up. has_custom_certificate (:obj:`bool`): :obj:`True`, if a custom certificate was provided for webhook certificate checks. pending_update_count (:obj:`int`): Number of updates awaiting delivery. ip_address (:obj:`str`): Optional. Currently used webhook IP address. - last_error_date (:obj:`int`): Optional. Unix time for the most recent error that happened - when trying to deliver an update via webhook. + last_error_date (:class:`datetime.datetime`): Optional. Datetime for the most recent + error that happened when trying to deliver an update via webhook. + + .. versionchanged:: NEXT.VERSION + |datetime_localization| last_error_message (:obj:`str`): Optional. Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook. max_connections (:obj:`int`): Optional. Maximum allowed number of simultaneous HTTPS @@ -86,10 +96,14 @@ class WebhookInfo(TelegramObject): * |tupleclassattrs| * |alwaystuple| - last_synchronization_error_date (:obj:`int`): Optional. Unix time of the most recent error - that happened when trying to synchronize available updates with Telegram datacenters. + last_synchronization_error_date (:class:`datetime.datetime`, optional): Datetime of the + most recent error that happened when trying to synchronize available updates with + Telegram datacenters. .. versionadded:: 20.0 + + .. versionchanged:: NEXT.VERSION + |datetime_localization| """ __slots__ = ( @@ -154,9 +168,12 @@ class WebhookInfo(TelegramObject): if not data: return None - data["last_error_date"] = from_timestamp(data.get("last_error_date")) + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + + data["last_error_date"] = from_timestamp(data.get("last_error_date"), tzinfo=loc_tzinfo) data["last_synchronization_error_date"] = from_timestamp( - data.get("last_synchronization_error_date") + data.get("last_synchronization_error_date"), tzinfo=loc_tzinfo ) return super().de_json(data=data, bot=bot) diff --git a/tests/_utils/test_datetime.py b/tests/_utils/test_datetime.py index 3776c3da9..ca89390bc 100644 --- a/tests/_utils/test_datetime.py +++ b/tests/_utils/test_datetime.py @@ -162,7 +162,7 @@ class TestDatetime: assert tg_dtm.from_timestamp(None) is None def test_from_timestamp_naive(self): - datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, tzinfo=None) + datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, tzinfo=dtm.timezone.utc) assert tg_dtm.from_timestamp(1573431976, tzinfo=None) == datetime def test_from_timestamp_aware(self, timezone): @@ -174,3 +174,8 @@ class TestDatetime: tg_dtm.from_timestamp(1573431976.1 - timezone.utcoffset(test_datetime).total_seconds()) == datetime ) + + def test_extract_tzinfo_from_defaults(self, tz_bot, bot, raw_bot): + assert tg_dtm.extract_tzinfo_from_defaults(tz_bot) == tz_bot.defaults.tzinfo + assert tg_dtm.extract_tzinfo_from_defaults(bot) is None + assert tg_dtm.extract_tzinfo_from_defaults(raw_bot) is None diff --git a/tests/conftest.py b/tests/conftest.py index d42629fc7..1f8171eda 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -134,7 +134,7 @@ async def raw_bot(bot_info): """Makes an regular Bot instance with the given bot_info""" async with PytestBot( bot_info["token"], - private_key=PRIVATE_KEY, + private_key=PRIVATE_KEY if TEST_WITH_OPT_DEPS else None, request=NonchalantHttpxRequest(8), get_updates_request=NonchalantHttpxRequest(1), ) as _bot: diff --git a/tests/test_chatinvitelink.py b/tests/test_chatinvitelink.py index 64110e651..0cbe9b412 100644 --- a/tests/test_chatinvitelink.py +++ b/tests/test_chatinvitelink.py @@ -21,7 +21,7 @@ import datetime import pytest from telegram import ChatInviteLink, User -from telegram._utils.datetime import to_timestamp +from telegram._utils.datetime import UTC, to_timestamp from tests.auxil.slots import mro_slots @@ -107,6 +107,33 @@ class TestChatInviteLinkWithoutRequest(TestChatInviteLinkBase): assert invite_link.name == self.name assert invite_link.pending_join_request_count == self.pending_join_request_count + def test_de_json_localization(self, tz_bot, bot, raw_bot, creator): + json_dict = { + "invite_link": self.link, + "creator": creator.to_dict(), + "creates_join_request": self.creates_join_request, + "is_primary": self.primary, + "is_revoked": self.revoked, + "expire_date": to_timestamp(self.expire_date), + "member_limit": self.member_limit, + "name": self.name, + "pending_join_request_count": str(self.pending_join_request_count), + } + + invite_link_raw = ChatInviteLink.de_json(json_dict, raw_bot) + invite_link_bot = ChatInviteLink.de_json(json_dict, bot) + invite_link_tz = ChatInviteLink.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + invite_offset = invite_link_tz.expire_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + invite_link_tz.expire_date.replace(tzinfo=None) + ) + + assert invite_link_raw.expire_date.tzinfo == UTC + assert invite_link_bot.expire_date.tzinfo == UTC + assert invite_offset == tz_bot_offset + def test_to_dict(self, invite_link): invite_link_dict = invite_link.to_dict() assert isinstance(invite_link_dict, dict) diff --git a/tests/test_chatjoinrequest.py b/tests/test_chatjoinrequest.py index b62ea5ec5..3f5d1e4ef 100644 --- a/tests/test_chatjoinrequest.py +++ b/tests/test_chatjoinrequest.py @@ -98,6 +98,26 @@ class TestChatJoinRequestWithoutRequest(TestChatJoinRequestBase): assert chat_join_request.bio == self.bio assert chat_join_request.invite_link == self.invite_link + def test_de_json_localization(self, tz_bot, bot, raw_bot, time): + json_dict = { + "chat": self.chat.to_dict(), + "from": self.from_user.to_dict(), + "date": to_timestamp(time), + "user_chat_id": self.from_user.id, + } + + chatjoin_req_raw = ChatJoinRequest.de_json(json_dict, raw_bot) + chatjoin_req_bot = ChatJoinRequest.de_json(json_dict, bot) + chatjoin_req_tz = ChatJoinRequest.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + chatjoin_req_offset = chatjoin_req_tz.date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(chatjoin_req_tz.date.replace(tzinfo=None)) + + assert chatjoin_req_raw.date.tzinfo == UTC + assert chatjoin_req_bot.date.tzinfo == UTC + assert chatjoin_req_offset == tz_bot_offset + def test_to_dict(self, chat_join_request, time): chat_join_request_dict = chat_join_request.to_dict() diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index 0455a8e23..265ac2a95 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -33,7 +33,7 @@ from telegram import ( Dice, User, ) -from telegram._utils.datetime import to_timestamp +from telegram._utils.datetime import UTC, to_timestamp from tests.auxil.slots import mro_slots ignored = ["self", "api_kwargs"] @@ -218,6 +218,24 @@ class TestChatMemberTypesWithoutRequest: for c_mem_type_at, const_c_mem_at in iter_args(chat_member_type, const_chat_member, True): assert c_mem_type_at == const_c_mem_at + def test_de_json_chatmemberbanned_localization(self, chat_member_type, tz_bot, bot, raw_bot): + # We only test two classes because the other three don't have datetimes in them. + if isinstance(chat_member_type, (ChatMemberBanned, ChatMemberRestricted)): + json_dict = make_json_dict(chat_member_type, include_optional_args=True) + chatmember_raw = ChatMember.de_json(json_dict, raw_bot) + chatmember_bot = ChatMember.de_json(json_dict, bot) + chatmember_tz = ChatMember.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + chatmember_offset = chatmember_tz.until_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + chatmember_tz.until_date.replace(tzinfo=None) + ) + + assert chatmember_raw.until_date.tzinfo == UTC + assert chatmember_bot.until_date.tzinfo == UTC + assert chatmember_offset == tz_bot_offset + def test_de_json_invalid_status(self, chat_member_type, bot): json_dict = {"status": "invalid", "user": CMDefaults.user.to_dict()} chat_member_type = ChatMember.de_json(json_dict, bot) diff --git a/tests/test_chatmemberupdated.py b/tests/test_chatmemberupdated.py index d2daada41..de3da1354 100644 --- a/tests/test_chatmemberupdated.py +++ b/tests/test_chatmemberupdated.py @@ -137,6 +137,32 @@ class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase): assert chat_member_updated.new_chat_member == new_chat_member assert chat_member_updated.invite_link == invite_link + def test_de_json_localization( + self, bot, raw_bot, tz_bot, user, chat, old_chat_member, new_chat_member, time, invite_link + ): + json_dict = { + "chat": chat.to_dict(), + "from": user.to_dict(), + "date": to_timestamp(time), + "old_chat_member": old_chat_member.to_dict(), + "new_chat_member": new_chat_member.to_dict(), + "invite_link": invite_link.to_dict(), + } + + chat_member_updated_bot = ChatMemberUpdated.de_json(json_dict, bot) + chat_member_updated_raw = ChatMemberUpdated.de_json(json_dict, raw_bot) + chat_member_updated_tz = ChatMemberUpdated.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + message_offset = chat_member_updated_tz.date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + chat_member_updated_tz.date.replace(tzinfo=None) + ) + + assert chat_member_updated_raw.date.tzinfo == UTC + assert chat_member_updated_bot.date.tzinfo == UTC + assert message_offset == tz_bot_offset + def test_to_dict(self, chat_member_updated): chat_member_updated_dict = chat_member_updated.to_dict() assert isinstance(chat_member_updated_dict, dict) diff --git a/tests/test_message.py b/tests/test_message.py index 38b347302..a47c085de 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -56,6 +56,7 @@ from telegram import ( Voice, WebAppData, ) +from telegram._utils.datetime import UTC from telegram.constants import ChatAction, ParseMode from telegram.ext import Defaults from tests._passport.test_passport import RAW_PASSPORT_DATA @@ -365,6 +366,46 @@ class TestMessageWithoutRequest(TestMessageBase): for slot in new.__slots__: assert not isinstance(new[slot], dict) + def test_de_json_localization(self, bot, raw_bot, tz_bot): + json_dict = { + "message_id": 12, + "from_user": None, + "date": int(datetime.now().timestamp()), + "chat": None, + "edit_date": int(datetime.now().timestamp()), + "forward_date": int(datetime.now().timestamp()), + } + + message_raw = Message.de_json(json_dict, raw_bot) + message_bot = Message.de_json(json_dict, bot) + message_tz = Message.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + date_offset = message_tz.date.utcoffset() + date_tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(message_tz.date.replace(tzinfo=None)) + + edit_date_offset = message_tz.edit_date.utcoffset() + edit_date_tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + message_tz.edit_date.replace(tzinfo=None) + ) + + forward_date_offset = message_tz.forward_date.utcoffset() + forward_date_tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + message_tz.forward_date.replace(tzinfo=None) + ) + + assert message_raw.date.tzinfo == UTC + assert message_bot.date.tzinfo == UTC + assert date_offset == date_tz_bot_offset + + assert message_raw.edit_date.tzinfo == UTC + assert message_bot.edit_date.tzinfo == UTC + assert edit_date_offset == edit_date_tz_bot_offset + + assert message_raw.forward_date.tzinfo == UTC + assert message_bot.forward_date.tzinfo == UTC + assert forward_date_offset == forward_date_tz_bot_offset + def test_equality(self): id_ = 1 a = Message(id_, self.date, self.chat, from_user=self.from_user) diff --git a/tests/test_poll.py b/tests/test_poll.py index 0250df8c7..d17f66736 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -20,7 +20,7 @@ from datetime import datetime, timedelta, timezone import pytest from telegram import MessageEntity, Poll, PollAnswer, PollOption, User -from telegram._utils.datetime import to_timestamp +from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import PollType from tests.auxil.slots import mro_slots @@ -208,6 +208,36 @@ class TestPollWithoutRequest(TestPollBase): assert abs(poll.close_date - self.close_date) < timedelta(seconds=1) assert to_timestamp(poll.close_date) == to_timestamp(self.close_date) + def test_de_json_localization(self, tz_bot, bot, raw_bot): + json_dict = { + "id": self.id_, + "question": self.question, + "options": [o.to_dict() for o in self.options], + "total_voter_count": self.total_voter_count, + "is_closed": self.is_closed, + "is_anonymous": self.is_anonymous, + "type": self.type, + "allows_multiple_answers": self.allows_multiple_answers, + "explanation": self.explanation, + "explanation_entities": [self.explanation_entities[0].to_dict()], + "open_period": self.open_period, + "close_date": to_timestamp(self.close_date), + } + + poll_raw = Poll.de_json(json_dict, raw_bot) + poll_bot = Poll.de_json(json_dict, bot) + poll_bot_tz = Poll.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + poll_bot_tz_offset = poll_bot_tz.close_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + poll_bot_tz.close_date.replace(tzinfo=None) + ) + + assert poll_raw.close_date.tzinfo == UTC + assert poll_bot.close_date.tzinfo == UTC + assert poll_bot_tz_offset == tz_bot_offset + def test_to_dict(self, poll): poll_dict = poll.to_dict() diff --git a/tests/test_videochat.py b/tests/test_videochat.py index 046159bf4..a3272c7b0 100644 --- a/tests/test_videochat.py +++ b/tests/test_videochat.py @@ -27,7 +27,7 @@ from telegram import ( VideoChatScheduled, VideoChatStarted, ) -from telegram._utils.datetime import to_timestamp +from telegram._utils.datetime import UTC, to_timestamp from tests.auxil.slots import mro_slots @@ -170,6 +170,23 @@ class TestVideoChatScheduledWithoutRequest: assert abs(video_chat_scheduled.start_date - self.start_date) < dtm.timedelta(seconds=1) + def test_de_json_localization(self, tz_bot, bot, raw_bot): + json_dict = {"start_date": to_timestamp(self.start_date)} + + videochat_raw = VideoChatScheduled.de_json(json_dict, raw_bot) + videochat_bot = VideoChatScheduled.de_json(json_dict, bot) + videochat_tz = VideoChatScheduled.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + videochat_offset = videochat_tz.start_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + videochat_tz.start_date.replace(tzinfo=None) + ) + + assert videochat_raw.start_date.tzinfo == UTC + assert videochat_bot.start_date.tzinfo == UTC + assert videochat_offset == tz_bot_offset + def test_to_dict(self): video_chat_scheduled = VideoChatScheduled(self.start_date) video_chat_scheduled_dict = video_chat_scheduled.to_dict() diff --git a/tests/test_webhookinfo.py b/tests/test_webhookinfo.py index 69139c26a..acba601ff 100644 --- a/tests/test_webhookinfo.py +++ b/tests/test_webhookinfo.py @@ -22,7 +22,7 @@ from datetime import datetime import pytest from telegram import LoginUrl, WebhookInfo -from telegram._utils.datetime import from_timestamp +from telegram._utils.datetime import UTC, from_timestamp from tests.auxil.slots import mro_slots @@ -102,6 +102,40 @@ class TestWebhookInfoWithoutRequest(TestWebhookInfoBase): none = WebhookInfo.de_json(None, bot) assert none is None + def test_de_json_localization(self, bot, raw_bot, tz_bot): + json_dict = { + "url": self.url, + "has_custom_certificate": self.has_custom_certificate, + "pending_update_count": self.pending_update_count, + "last_error_date": self.last_error_date, + "max_connections": self.max_connections, + "allowed_updates": self.allowed_updates, + "ip_address": self.ip_address, + "last_synchronization_error_date": self.last_synchronization_error_date, + } + webhook_info_bot = WebhookInfo.de_json(json_dict, bot) + webhook_info_raw = WebhookInfo.de_json(json_dict, raw_bot) + webhook_info_tz = WebhookInfo.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + last_error_date_offset = webhook_info_tz.last_error_date.utcoffset() + last_error_tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + webhook_info_tz.last_error_date.replace(tzinfo=None) + ) + + sync_error_date_offset = webhook_info_tz.last_synchronization_error_date.utcoffset() + sync_error_date_tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + webhook_info_tz.last_synchronization_error_date.replace(tzinfo=None) + ) + + assert webhook_info_raw.last_error_date.tzinfo == UTC + assert webhook_info_bot.last_error_date.tzinfo == UTC + assert last_error_date_offset == last_error_tz_bot_offset + + assert webhook_info_raw.last_synchronization_error_date.tzinfo == UTC + assert webhook_info_bot.last_synchronization_error_date.tzinfo == UTC + assert sync_error_date_offset == sync_error_date_tz_bot_offset + def test_always_tuple_allowed_updates(self): webhook_info = WebhookInfo( self.url, self.has_custom_certificate, self.pending_update_count