From bb3138cc9cd06efdd2ddaa1f0e710ff83078efff Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 29 Mar 2026 06:56:22 -0700 Subject: [PATCH 01/11] WIP: Start centralizing de_json --- src/telegram/_business.py | 52 ------- src/telegram/_callbackquery.py | 10 -- src/telegram/_chatboost.py | 43 ----- src/telegram/_chatjoinrequest.py | 15 -- src/telegram/_message.py | 260 +++++-------------------------- src/telegram/_telegramobject.py | 145 ++++++++++++++++- 6 files changed, 183 insertions(+), 342 deletions(-) diff --git a/src/telegram/_business.py b/src/telegram/_business.py index 5b2026c62d0..b7861b8c503 100644 --- a/src/telegram/_business.py +++ b/src/telegram/_business.py @@ -266,20 +266,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "BusinessConnection": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo) - data["user"] = de_json_optional(data.get("user"), User, bot) - data["rights"] = de_json_optional(data.get("rights"), BusinessBotRights, bot) - - return super().de_json(data=data, bot=bot) - class BusinessMessagesDeleted(TelegramObject): """ @@ -333,15 +319,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "BusinessMessagesDeleted": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - - return super().de_json(data=data, bot=bot) - class BusinessIntro(TelegramObject): """ @@ -387,15 +364,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "BusinessIntro": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot) - - return super().de_json(data=data, bot=bot) - class BusinessLocation(TelegramObject): """ @@ -436,15 +404,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "BusinessLocation": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["location"] = de_json_optional(data.get("location"), Location, bot) - - return super().de_json(data=data, bot=bot) - class BusinessOpeningHoursInterval(TelegramObject): """ @@ -678,14 +637,3 @@ def is_open(self, datetime: dtm.datetime) -> bool: return True return False - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "BusinessOpeningHours": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["opening_hours"] = de_list_optional( - data.get("opening_hours"), BusinessOpeningHoursInterval, bot - ) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_callbackquery.py b/src/telegram/_callbackquery.py index 9004de89388..9cabb5daf88 100644 --- a/src/telegram/_callbackquery.py +++ b/src/telegram/_callbackquery.py @@ -153,16 +153,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "CallbackQuery": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["from_user"] = de_json_optional(data.pop("from", None), User, bot) - data["message"] = de_json_optional(data.get("message"), Message, bot) - - return super().de_json(data=data, bot=bot) - async def answer( self, text: str | None = None, diff --git a/src/telegram/_chatboost.py b/src/telegram/_chatboost.py index 2386f2e6a5c..4bc184900aa 100644 --- a/src/telegram/_chatboost.py +++ b/src/telegram/_chatboost.py @@ -285,18 +285,6 @@ def __init__( self._id_attrs = (self.boost_id, self.add_date, self.expiration_date, self.source) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatBoost": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["source"] = de_json_optional(data.get("source"), ChatBoostSource, bot) - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["add_date"] = from_timestamp(data.get("add_date"), tzinfo=loc_tzinfo) - data["expiration_date"] = from_timestamp(data.get("expiration_date"), tzinfo=loc_tzinfo) - - return super().de_json(data=data, bot=bot) - class ChatBoostUpdated(TelegramObject): """This object represents a boost added to a chat or changed. @@ -332,16 +320,6 @@ def __init__( self._id_attrs = (self.chat.id, self.boost) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatBoostUpdated": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - data["boost"] = de_json_optional(data.get("boost"), ChatBoost, bot) - - return super().de_json(data=data, bot=bot) - class ChatBoostRemoved(TelegramObject): """ @@ -386,18 +364,6 @@ def __init__( self._id_attrs = (self.chat, self.boost_id, self.remove_date, self.source) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatBoostRemoved": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - data["source"] = de_json_optional(data.get("source"), ChatBoostSource, bot) - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["remove_date"] = from_timestamp(data.get("remove_date"), tzinfo=loc_tzinfo) - - return super().de_json(data=data, bot=bot) - class UserChatBoosts(TelegramObject): """This object represents a list of boosts added to a chat by a user. @@ -429,12 +395,3 @@ def __init__( self._id_attrs = (self.boosts,) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "UserChatBoosts": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["boosts"] = de_list_optional(data.get("boosts"), ChatBoost, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_chatjoinrequest.py b/src/telegram/_chatjoinrequest.py index 75c2087d794..7defa6f1689 100644 --- a/src/telegram/_chatjoinrequest.py +++ b/src/telegram/_chatjoinrequest.py @@ -130,21 +130,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatJoinRequest": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - data["from_user"] = de_json_optional(data.pop("from", None), User, bot) - data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo) - data["invite_link"] = de_json_optional(data.get("invite_link"), ChatInviteLink, bot) - - return super().de_json(data=data, bot=bot) - async def approve( self, *, diff --git a/src/telegram/_message.py b/src/telegram/_message.py index c0d018ec406..00c058530cd 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -23,7 +23,7 @@ import re from collections.abc import Sequence from html import escape -from typing import TYPE_CHECKING, TypedDict +from typing import TYPE_CHECKING, ClassVar, TypedDict from telegram._chat import Chat from telegram._chatbackground import ChatBackground @@ -73,7 +73,7 @@ from telegram._telegramobject import TelegramObject from telegram._uniquegift import UniqueGiftInfo from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import de_json_optional, parse_sequence_arg from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp, to_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram._utils.entities import parse_message_entities, parse_message_entity @@ -195,30 +195,43 @@ def is_accessible(self) -> bool: return self.date != ZERO_DATE @classmethod - def _de_json( + def de_json( cls, - data: JSONDict | None, + data: JSONDict, bot: "Bot | None" = None, - api_kwargs: JSONDict | None = None, - ) -> "MaybeInaccessibleMessage | None": + ) -> "MaybeInaccessibleMessage": """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - + # Dispatch *before* the plan runs so that the raw ``date`` value is still + # an integer and can be compared to 0. if cls is MaybeInaccessibleMessage: - if data["date"] == 0: + data = cls._parse_data(data) + if data.get("date") == 0: return InaccessibleMessage.de_json(data=data, bot=bot) return Message.de_json(data=data, bot=bot) + return super().de_json(data=data, bot=bot) - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) + @classmethod + def _de_json( + cls, + data: JSONDict, + bot: "Bot | None" = None, + api_kwargs: JSONDict | None = None, + ) -> "MaybeInaccessibleMessage": + """See :meth:`telegram.TelegramObject.de_json`.""" + # Idempotent: the plan may have already converted these fields. + date_val = data.get("date") + if date_val is not None and not isinstance(date_val, dtm.datetime): + print("this was an int, converting to datetime in MaybeInaccessibleMessage") + if date_val == 0: + data["date"] = ZERO_DATE + else: + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["date"] = from_timestamp(date_val, tzinfo=loc_tzinfo) - # this is to include the Literal from InaccessibleMessage - if data["date"] == 0: - data["date"] = ZERO_DATE - else: - data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo) + if not isinstance(data.get("chat"), Chat): + print("De-jsoning chat in MaybeInaccessibleMessage") + data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) @@ -257,6 +270,18 @@ def __init__( class Message(MaybeInaccessibleMessage): + __REMOVED_API_FIELDS__: ClassVar[frozenset[str]] = frozenset( + { + "user_shared", + "forward_from", + "forward_from_chat", + "forward_from_message_id", + "forward_signature", + "forward_sender_name", + "forward_date", + } + ) + # fmt: off """This object represents a message. @@ -1506,207 +1531,6 @@ def link(self) -> str | None: return baselink return None - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Message": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["from_user"] = de_json_optional(data.pop("from", None), User, bot) - data["sender_chat"] = de_json_optional(data.get("sender_chat"), Chat, bot) - data["entities"] = de_list_optional(data.get("entities"), MessageEntity, bot) - data["caption_entities"] = de_list_optional( - data.get("caption_entities"), MessageEntity, bot - ) - data["reply_to_message"] = de_json_optional(data.get("reply_to_message"), Message, bot) - data["edit_date"] = from_timestamp(data.get("edit_date"), tzinfo=loc_tzinfo) - data["audio"] = de_json_optional(data.get("audio"), Audio, bot) - data["document"] = de_json_optional(data.get("document"), Document, bot) - data["animation"] = de_json_optional(data.get("animation"), Animation, bot) - data["game"] = de_json_optional(data.get("game"), Game, bot) - data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot) - data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot) - data["story"] = de_json_optional(data.get("story"), Story, bot) - data["video"] = de_json_optional(data.get("video"), Video, bot) - data["voice"] = de_json_optional(data.get("voice"), Voice, bot) - data["video_note"] = de_json_optional(data.get("video_note"), VideoNote, bot) - data["contact"] = de_json_optional(data.get("contact"), Contact, bot) - data["location"] = de_json_optional(data.get("location"), Location, bot) - data["venue"] = de_json_optional(data.get("venue"), Venue, bot) - data["new_chat_members"] = de_list_optional(data.get("new_chat_members"), User, bot) - data["left_chat_member"] = de_json_optional(data.get("left_chat_member"), User, bot) - data["new_chat_photo"] = de_list_optional(data.get("new_chat_photo"), PhotoSize, bot) - data["message_auto_delete_timer_changed"] = de_json_optional( - data.get("message_auto_delete_timer_changed"), MessageAutoDeleteTimerChanged, bot - ) - data["pinned_message"] = de_json_optional( - data.get("pinned_message"), MaybeInaccessibleMessage, bot - ) - data["invoice"] = de_json_optional(data.get("invoice"), Invoice, bot) - data["successful_payment"] = de_json_optional( - data.get("successful_payment"), SuccessfulPayment, bot - ) - data["passport_data"] = de_json_optional(data.get("passport_data"), PassportData, bot) - data["poll"] = de_json_optional(data.get("poll"), Poll, bot) - data["dice"] = de_json_optional(data.get("dice"), Dice, bot) - data["via_bot"] = de_json_optional(data.get("via_bot"), User, bot) - data["proximity_alert_triggered"] = de_json_optional( - data.get("proximity_alert_triggered"), ProximityAlertTriggered, bot - ) - data["reply_markup"] = de_json_optional( - data.get("reply_markup"), InlineKeyboardMarkup, bot - ) - data["video_chat_scheduled"] = de_json_optional( - data.get("video_chat_scheduled"), VideoChatScheduled, bot - ) - data["video_chat_started"] = de_json_optional( - data.get("video_chat_started"), VideoChatStarted, bot - ) - data["video_chat_ended"] = de_json_optional( - data.get("video_chat_ended"), VideoChatEnded, bot - ) - data["video_chat_participants_invited"] = de_json_optional( - data.get("video_chat_participants_invited"), VideoChatParticipantsInvited, bot - ) - data["web_app_data"] = de_json_optional(data.get("web_app_data"), WebAppData, bot) - data["forum_topic_closed"] = de_json_optional( - data.get("forum_topic_closed"), ForumTopicClosed, bot - ) - data["forum_topic_created"] = de_json_optional( - data.get("forum_topic_created"), ForumTopicCreated, bot - ) - data["forum_topic_reopened"] = de_json_optional( - data.get("forum_topic_reopened"), ForumTopicReopened, bot - ) - data["forum_topic_edited"] = de_json_optional( - data.get("forum_topic_edited"), ForumTopicEdited, bot - ) - data["general_forum_topic_hidden"] = de_json_optional( - data.get("general_forum_topic_hidden"), GeneralForumTopicHidden, bot - ) - data["general_forum_topic_unhidden"] = de_json_optional( - data.get("general_forum_topic_unhidden"), GeneralForumTopicUnhidden, bot - ) - data["write_access_allowed"] = de_json_optional( - data.get("write_access_allowed"), WriteAccessAllowed, bot - ) - data["users_shared"] = de_json_optional(data.get("users_shared"), UsersShared, bot) - data["chat_shared"] = de_json_optional(data.get("chat_shared"), ChatShared, bot) - data["chat_background_set"] = de_json_optional( - data.get("chat_background_set"), ChatBackground, bot - ) - data["paid_media"] = de_json_optional(data.get("paid_media"), PaidMediaInfo, bot) - data["refunded_payment"] = de_json_optional( - data.get("refunded_payment"), RefundedPayment, bot - ) - data["gift"] = de_json_optional(data.get("gift"), GiftInfo, bot) - data["unique_gift"] = de_json_optional(data.get("unique_gift"), UniqueGiftInfo, bot) - data["paid_message_price_changed"] = de_json_optional( - data.get("paid_message_price_changed"), PaidMessagePriceChanged, bot - ) - - # Unfortunately, this needs to be here due to cyclic imports - from telegram._giveaway import ( # pylint: disable=C0415 # noqa: PLC0415 - Giveaway, - GiveawayCompleted, - GiveawayCreated, - GiveawayWinners, - ) - from telegram._messageorigin import ( # pylint: disable=C0415 # noqa: PLC0415 - MessageOrigin, - ) - from telegram._reply import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 - ExternalReplyInfo, - TextQuote, - ) - from telegram._suggestedpost import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 - SuggestedPostApprovalFailed, - SuggestedPostApproved, - SuggestedPostDeclined, - SuggestedPostInfo, - SuggestedPostPaid, - SuggestedPostRefunded, - ) - - data["giveaway"] = de_json_optional(data.get("giveaway"), Giveaway, bot) - data["giveaway_completed"] = de_json_optional( - data.get("giveaway_completed"), GiveawayCompleted, bot - ) - data["giveaway_created"] = de_json_optional( - data.get("giveaway_created"), GiveawayCreated, bot - ) - data["giveaway_winners"] = de_json_optional( - data.get("giveaway_winners"), GiveawayWinners, bot - ) - data["link_preview_options"] = de_json_optional( - data.get("link_preview_options"), LinkPreviewOptions, bot - ) - data["external_reply"] = de_json_optional( - data.get("external_reply"), ExternalReplyInfo, bot - ) - data["quote"] = de_json_optional(data.get("quote"), TextQuote, bot) - data["forward_origin"] = de_json_optional(data.get("forward_origin"), MessageOrigin, bot) - data["reply_to_story"] = de_json_optional(data.get("reply_to_story"), Story, bot) - data["boost_added"] = de_json_optional(data.get("boost_added"), ChatBoostAdded, bot) - data["sender_business_bot"] = de_json_optional(data.get("sender_business_bot"), User, bot) - data["direct_message_price_changed"] = de_json_optional( - data.get("direct_message_price_changed"), DirectMessagePriceChanged, bot - ) - data["checklist"] = de_json_optional(data.get("checklist"), Checklist, bot) - data["checklist_tasks_done"] = de_json_optional( - data.get("checklist_tasks_done"), ChecklistTasksDone, bot - ) - data["checklist_tasks_added"] = de_json_optional( - data.get("checklist_tasks_added"), ChecklistTasksAdded, bot - ) - data["direct_messages_topic"] = de_json_optional( - data.get("direct_messages_topic"), DirectMessagesTopic, bot - ) - data["suggested_post_declined"] = de_json_optional( - data.get("suggested_post_declined"), SuggestedPostDeclined, bot - ) - data["suggested_post_paid"] = de_json_optional( - data.get("suggested_post_paid"), SuggestedPostPaid, bot - ) - data["suggested_post_refunded"] = de_json_optional( - data.get("suggested_post_refunded"), SuggestedPostRefunded, bot - ) - data["suggested_post_info"] = de_json_optional( - data.get("suggested_post_info"), SuggestedPostInfo, bot - ) - data["suggested_post_approved"] = de_json_optional( - data.get("suggested_post_approved"), SuggestedPostApproved, bot - ) - data["suggested_post_approval_failed"] = de_json_optional( - data.get("suggested_post_approval_failed"), SuggestedPostApprovalFailed, bot - ) - data["gift_upgrade_sent"] = de_json_optional(data.get("gift_upgrade_sent"), GiftInfo, bot) - data["chat_owner_changed"] = de_json_optional( - data.get("chat_owner_changed"), ChatOwnerChanged, bot - ) - data["chat_owner_left"] = de_json_optional(data.get("chat_owner_left"), ChatOwnerLeft, bot) - - api_kwargs = {} - # This is a deprecated field that TG still returns for backwards compatibility - # Let's filter it out to speed up the de-json process - for key in ( - "user_shared", - "forward_from", - "forward_from_chat", - "forward_from_message_id", - "forward_signature", - "forward_sender_name", - "forward_date", - ): - if entry := data.get(key): - api_kwargs = {key: entry} - - return super()._de_json( # type: ignore[return-value] - data=data, bot=bot, api_kwargs=api_kwargs - ) - @property def effective_attachment( self, diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index 4098c75c37e..5a9559cde8e 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -20,16 +20,18 @@ import contextlib import datetime as dtm +import importlib import inspect import json -from collections.abc import Iterator, Mapping, Sized +import types as _types +from collections.abc import Iterator, Mapping, Sequence, Sized from contextlib import contextmanager from copy import deepcopy from itertools import chain from types import MappingProxyType -from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast, get_args, get_origin -from telegram._utils.datetime import to_timestamp +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp, to_timestamp from telegram._utils.defaultvalue import DefaultValue from telegram._utils.types import JSONDict from telegram._utils.warnings import warn @@ -41,6 +43,20 @@ Tele = TypeVar("Tele", bound="TelegramObject") +def _telegram_ns() -> dict[str, object]: + """Return the full ``telegram`` package namespace for annotation resolution.""" + return vars(importlib.import_module("telegram")) + + +def _unwrap_optional(ann: object) -> object: + """``X | None`` → ``X``. Any other annotation is returned unchanged.""" + if isinstance(ann, _types.UnionType): + non_none = [a for a in ann.__args__ if a is not type(None)] + if len(non_none) == 1: + return non_none[0] + return ann + + class TelegramObject: """Base class for most Telegram objects. @@ -88,6 +104,23 @@ class TelegramObject: # unless it's overridden __INIT_PARAMS_CHECK: type["TelegramObject"] | None = None + # Per-class declarative de_json plan (built lazily on first de_json call). + # Maps parameter name → transform callable. Each subclass gets its own + # plan via _build_plan(); the ``cls.__dict__`` check in de_json ensures + # inherited plans are never confused with the class's own plan. + __DE_JSON_PLAN__: ClassVar[dict[str, Any]] = {} + + # Subclasses may declare field names that Telegram still returns for backwards + # compatibility but that are no longer part of the PTB model. Those fields will + # be intercepted by de_json and forwarded into api_kwargs before construction. + __REMOVED_API_FIELDS__: ClassVar[frozenset[str]] = frozenset() + + # def __init_subclass__(cls, **kwargs) -> None: + # super().__init_subclass__(**kwargs) + + # print("Building de_json plan for", cls.__name__) + # cls.__DE_JSON_PLAN__ = cls._build_plan() + def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: # Setting _frozen to `False` here means that classes without arguments still need to # implement __init__. However, with `True` would mean increased usage of @@ -385,6 +418,75 @@ def _parse_data(data: JSONDict) -> JSONDict: """ return data.copy() + @classmethod + def _build_plan(cls) -> dict[str, Any]: + """Build the de_json transformation plan from ``__init__`` type annotations. + + Called once per class on the first :meth:`de_json` invocation. By that time + every module is fully loaded so forward-reference resolution always succeeds. + + The resulting ``dict`` maps parameter names (e.g. ``"from_user"``, + ``"date"``, ``"entities"``) to a lambda ``(value, bot) → converted_value``. + Three kinds of transforms are recognised: + + * ``datetime`` fields → :func:`~telegram._utils.datetime.from_timestamp` + * ``TelegramObject`` fields → ``TargetCls.de_json(...)`` + * ``Sequence[TelegramObject]`` fields → ``TargetCls.de_list(...)`` + """ + init_fn = cls.__dict__.get("__init__") + if init_fn is None: + # No own __init__: inherit the nearest ancestor's plan. This is true for e.g. _ChatBase + parent = cls.__mro__[1] + print("No __init__ for", cls.__name__, "inheriting from", parent.__name__) + if "__DE_JSON_PLAN__" not in parent.__dict__: + parent._build_plan() + cls.__DE_JSON_PLAN__ = parent.__DE_JSON_PLAN__ + return cls.__DE_JSON_PLAN__ + + plan: dict[str, Any] = {} + globalns: dict[str, object] = getattr(init_fn, "__globals__", {}) + tg_ns = _telegram_ns() + + for name, param in inspect.signature( + init_fn, eval_str=True, globals=globalns, locals=tg_ns + ).parameters.items(): + if name in ("self", "api_kwargs") or param.kind in ( + param.VAR_POSITIONAL, + param.VAR_KEYWORD, + ): + continue + + ann = param.annotation + inner = _unwrap_optional(ann) + origin = get_origin(inner) + # Possible optimization: extract tzinfo here once by making this accept `bot` argument. + + if inner is dtm.datetime: + plan[name] = lambda value, bot: ( + value + if isinstance(value, dtm.datetime) + else from_timestamp(value, tzinfo=extract_tzinfo_from_defaults(bot)) + ) + elif isinstance(inner, type) and issubclass(inner, TelegramObject): + plan[name] = lambda v, b, _c=inner: _c.de_json(v, b) if isinstance(v, dict) else v + elif origin is Sequence: + args = get_args(inner) + # args: tuple[object, ...] = getattr(inner, "__args__", ()) + if not args: + continue + item_type: object = args[0] + # if isinstance(item_type, str): + # print(f"Resolving forward reference for {cls.__name__}.{name}: {item_type}") + # with contextlib.suppress(Exception): + # item_type = eval(item_type, globalns, tg_ns) # noqa: S307 + if isinstance(item_type, type) and issubclass(item_type, TelegramObject): + plan[name] = lambda v, b, _c=item_type: ( + _c.de_list(v, b) if isinstance(v, list) else v + ) + + cls.__DE_JSON_PLAN__ = plan + return plan + @classmethod def _de_json( cls: type[Tele_co], @@ -430,7 +532,42 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel The Telegram object. """ - return cls._de_json(data=data, bot=bot) + data = cls._parse_data(data) + + # Move removed/legacy API fields into api_kwargs + api_kwargs: JSONDict | None = None + if cls.__REMOVED_API_FIELDS__: + print(f"checking for removed API fields for {cls.__name__}") + removed = {f: data.pop(f) for f in cls.__REMOVED_API_FIELDS__ if f in data} + if removed: + api_kwargs = removed + + # Make the plan for de_json. This is done only on the first call to de_json for each class. + if "__DE_JSON_PLAN__" not in cls.__dict__: + print(f"building plan for {cls.__name__}") + cls._build_plan() + plan = cls.__DE_JSON_PLAN__ + + # Rename "from" → "from_user" before the transform loop so we can + # iterate data keys without adding/removing entries inside the loop. + if "from_user" in plan: # membership checks are O(1) + if "from" in data: + data["from_user"] = data.pop("from") + elif "from_user" not in data: + data["from_user"] = None + + # Only for classes with a plan. We avoid looping through the data keys for no plan classes. + if plan: + # This is O(M). This part is unavoidable. + print(f"Data length: {len(data)}, plan length: {len(plan)}") + for key in data: + if key in plan and data[key] is not None: + print("executing plan for", key) + if data[key] is None: + print(f"skipping {key} because value is None") + data[key] = plan[key](data[key], bot) + + return cls._de_json(data=data, bot=bot, api_kwargs=api_kwargs) @classmethod def de_list( From d0d7e19b4f1ad7f9b076ac99cfdd25a5f1fcc70a Mon Sep 17 00:00:00 2001 From: harshil21 <37377066+harshil21@users.noreply.github.com> Date: Sun, 29 Mar 2026 13:59:35 +0000 Subject: [PATCH 02/11] Add chango fragment for PR #5186 --- changes/unreleased/5186.VngJMeztfXKDXAhcqTxJjK.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changes/unreleased/5186.VngJMeztfXKDXAhcqTxJjK.toml diff --git a/changes/unreleased/5186.VngJMeztfXKDXAhcqTxJjK.toml b/changes/unreleased/5186.VngJMeztfXKDXAhcqTxJjK.toml new file mode 100644 index 00000000000..dcb5de58488 --- /dev/null +++ b/changes/unreleased/5186.VngJMeztfXKDXAhcqTxJjK.toml @@ -0,0 +1,5 @@ +other = "PoC: Start centralizing `de_json`" +[[pull_requests]] +uid = "5186" +author_uids = ["harshil21"] +closes_threads = [] From 21a66b7e1bd9af992102a019641f23522c584862 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:44:02 -0700 Subject: [PATCH 03/11] WIP: Flesh out the implementation a little more Some tests are still failing --- src/telegram/_chatbackground.py | 9 -- src/telegram/_chatfullinfo.py | 49 ---------- src/telegram/_chatinvitelink.py | 19 +--- src/telegram/_chatlocation.py | 10 -- src/telegram/_chatmemberupdated.py | 17 ---- src/telegram/_chatowner.py | 19 ---- src/telegram/_chatpermissions.py | 19 ++-- src/telegram/_checklists.py | 64 +------------ src/telegram/_choseninlineresult.py | 13 --- src/telegram/_directmessagestopic.py | 10 -- src/telegram/_files/_basemedium.py | 1 - src/telegram/_files/_basethumbedmedium.py | 28 ++---- src/telegram/_files/sticker.py | 40 ++------ src/telegram/_files/venue.py | 10 -- src/telegram/_files/video.py | 13 --- src/telegram/_games/game.py | 11 --- src/telegram/_games/gamehighscore.py | 10 -- src/telegram/_gifts.py | 28 ------ src/telegram/_giveaway.py | 50 +--------- src/telegram/_inline/inlinekeyboardbutton.py | 16 ---- src/telegram/_inline/inlinekeyboardmarkup.py | 1 + src/telegram/_inline/inlinequery.py | 11 --- .../_inline/inlinequeryresultsbutton.py | 9 -- .../_inline/inputinvoicemessagecontent.py | 9 -- src/telegram/_inline/preparedinlinemessage.py | 12 --- src/telegram/_keyboardbutton.py | 30 ++---- src/telegram/_keyboardbuttonrequest.py | 14 --- src/telegram/_menubutton.py | 10 -- src/telegram/_messageentity.py | 15 --- src/telegram/_messagereactionupdated.py | 32 ------- src/telegram/_ownedgift.py | 29 ------ src/telegram/_paidmedia.py | 30 ------ src/telegram/_passport/credentials.py | 48 ---------- .../_passport/encryptedpassportelement.py | 15 --- src/telegram/_passport/passportdata.py | 10 -- src/telegram/_passport/passportfile.py | 12 --- src/telegram/_payment/orderinfo.py | 12 --- src/telegram/_payment/precheckoutquery.py | 11 --- src/telegram/_payment/shippingquery.py | 13 --- src/telegram/_payment/stars/affiliateinfo.py | 11 --- .../_payment/stars/revenuewithdrawalstate.py | 14 --- .../_payment/stars/startransactions.py | 23 ----- .../_payment/stars/transactionpartner.py | 4 +- src/telegram/_payment/successfulpayment.py | 18 ---- src/telegram/_poll.py | 54 +---------- src/telegram/_proximityalerttriggered.py | 11 --- src/telegram/_reaction.py | 10 -- src/telegram/_reply.py | 55 ----------- src/telegram/_shared.py | 37 ++------ src/telegram/_story.py | 8 -- src/telegram/_suggestedpost.py | 91 ------------------- src/telegram/_telegramobject.py | 16 ++-- src/telegram/_uniquegift.py | 54 ----------- src/telegram/_update.py | 56 ------------ src/telegram/_userprofileaudios.py | 9 -- src/telegram/_userprofilephotos.py | 9 -- src/telegram/_videochat.py | 26 +----- src/telegram/_webhookinfo.py | 16 ---- tests/_files/test_sticker.py | 68 ++++++++------ 59 files changed, 91 insertions(+), 1258 deletions(-) diff --git a/src/telegram/_chatbackground.py b/src/telegram/_chatbackground.py index 5b71259d58e..f5bde6f05f7 100644 --- a/src/telegram/_chatbackground.py +++ b/src/telegram/_chatbackground.py @@ -522,12 +522,3 @@ def __init__( self._id_attrs = (self.type,) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatBackground": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["type"] = de_json_optional(data.get("type"), BackgroundType, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_chatfullinfo.py b/src/telegram/_chatfullinfo.py index 7142b5feb6a..9fd551546b9 100644 --- a/src/telegram/_chatfullinfo.py +++ b/src/telegram/_chatfullinfo.py @@ -623,52 +623,3 @@ def message_auto_delete_time(self) -> int | dtm.timedelta | None: return get_timedelta_value( self._message_auto_delete_time, attribute="message_auto_delete_time" ) - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatFullInfo": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["emoji_status_expiration_date"] = from_timestamp( - data.get("emoji_status_expiration_date"), tzinfo=loc_tzinfo - ) - - data["photo"] = de_json_optional(data.get("photo"), ChatPhoto, bot) - data["accepted_gift_types"] = de_json_optional( - data.get("accepted_gift_types"), AcceptedGiftTypes, bot - ) - - from telegram import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 - BusinessIntro, - BusinessLocation, - BusinessOpeningHours, - Message, - ) - - data["pinned_message"] = de_json_optional(data.get("pinned_message"), Message, bot) - data["permissions"] = de_json_optional(data.get("permissions"), ChatPermissions, bot) - data["location"] = de_json_optional(data.get("location"), ChatLocation, bot) - data["available_reactions"] = de_list_optional( - data.get("available_reactions"), ReactionType, bot - ) - data["birthdate"] = de_json_optional(data.get("birthdate"), Birthdate, bot) - data["personal_chat"] = de_json_optional(data.get("personal_chat"), Chat, bot) - data["business_intro"] = de_json_optional(data.get("business_intro"), BusinessIntro, bot) - data["business_location"] = de_json_optional( - data.get("business_location"), BusinessLocation, bot - ) - data["business_opening_hours"] = de_json_optional( - data.get("business_opening_hours"), BusinessOpeningHours, bot - ) - data["parent_chat"] = de_json_optional(data.get("parent_chat"), Chat, bot) - - data["rating"] = de_json_optional(data.get("rating"), UserRating, bot) - data["unique_gift_colors"] = de_json_optional( - data.get("unique_gift_colors"), UniqueGiftColors, bot - ) - data["first_profile_audio"] = de_json_optional(data.get("first_profile_audio"), Audio, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_chatinvitelink.py b/src/telegram/_chatinvitelink.py index 880349fd0fb..cbb1be6c188 100644 --- a/src/telegram/_chatinvitelink.py +++ b/src/telegram/_chatinvitelink.py @@ -24,11 +24,7 @@ from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import de_json_optional, to_timedelta -from telegram._utils.datetime import ( - extract_tzinfo_from_defaults, - from_timestamp, - get_timedelta_value, -) +from telegram._utils.datetime import get_timedelta_value from telegram._utils.types import JSONDict, TimePeriod if TYPE_CHECKING: @@ -191,16 +187,3 @@ def __init__( @property def subscription_period(self) -> int | dtm.timedelta | None: return get_timedelta_value(self._subscription_period, attribute="subscription_period") - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatInviteLink": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["creator"] = de_json_optional(data.get("creator"), User, bot) - data["expire_date"] = from_timestamp(data.get("expire_date", None), tzinfo=loc_tzinfo) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_chatlocation.py b/src/telegram/_chatlocation.py index b93cfc8a624..0788341fe1f 100644 --- a/src/telegram/_chatlocation.py +++ b/src/telegram/_chatlocation.py @@ -23,7 +23,6 @@ from telegram import constants from telegram._files.location import Location from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -68,15 +67,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatLocation": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["location"] = de_json_optional(data.get("location"), Location, bot) - - return super().de_json(data=data, bot=bot) - MIN_ADDRESS: Final[int] = constants.LocationLimit.MIN_CHAT_LOCATION_ADDRESS """:const:`telegram.constants.LocationLimit.MIN_CHAT_LOCATION_ADDRESS` diff --git a/src/telegram/_chatmemberupdated.py b/src/telegram/_chatmemberupdated.py index 2434376d459..98c4b9b7088 100644 --- a/src/telegram/_chatmemberupdated.py +++ b/src/telegram/_chatmemberupdated.py @@ -142,23 +142,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatMemberUpdated": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - data["from_user"] = de_json_optional(data.pop("from", None), User, bot) - data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo) - data["old_chat_member"] = de_json_optional(data.get("old_chat_member"), ChatMember, bot) - data["new_chat_member"] = de_json_optional(data.get("new_chat_member"), ChatMember, bot) - data["invite_link"] = de_json_optional(data.get("invite_link"), ChatInviteLink, bot) - - return super().de_json(data=data, bot=bot) - def _get_attribute_difference(self, attribute: str) -> tuple[object, object]: try: old = self.old_chat_member[attribute] diff --git a/src/telegram/_chatowner.py b/src/telegram/_chatowner.py index 1d8ce6619bd..e152082bb4e 100644 --- a/src/telegram/_chatowner.py +++ b/src/telegram/_chatowner.py @@ -22,7 +22,6 @@ from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -60,15 +59,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatOwnerChanged": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["new_owner"] = de_json_optional(data.get("new_owner"), User, bot) - - return super().de_json(data=data, bot=bot) - class ChatOwnerLeft(TelegramObject): """This object represents a service message about the chat owner leaving the chat. @@ -102,12 +92,3 @@ def __init__( self._id_attrs = (self.new_owner,) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatOwnerLeft": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["new_owner"] = de_json_optional(data.get("new_owner"), User, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_chatpermissions.py b/src/telegram/_chatpermissions.py index 2116f4f4c33..6fd8b134f66 100644 --- a/src/telegram/_chatpermissions.py +++ b/src/telegram/_chatpermissions.py @@ -166,6 +166,12 @@ class ChatPermissions(TelegramObject): "can_send_voice_notes", ) + __REMOVED_API_FIELDS__ = frozenset( + { + "can_send_media_messages", + } + ) + def __init__( self, can_send_messages: bool | None = None, @@ -245,16 +251,3 @@ def no_permissions(cls) -> "ChatPermissions": .. versionadded:: 20.0 """ return cls(*(False,) * len(cls.__slots__)) - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatPermissions": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - api_kwargs = {} - # This is a deprecated field that TG still returns for backwards compatibility - # Let's filter it out to speed up the de-json process - if data.get("can_send_media_messages") is not None: - api_kwargs["can_send_media_messages"] = data.pop("can_send_media_messages") - - return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) diff --git a/src/telegram/_checklists.py b/src/telegram/_checklists.py index 16b7676eb5b..aab0d7752d0 100644 --- a/src/telegram/_checklists.py +++ b/src/telegram/_checklists.py @@ -26,14 +26,12 @@ from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.entities import parse_message_entities, parse_message_entity from telegram._utils.types import JSONDict -from telegram.constants import ZERO_DATE if TYPE_CHECKING: - from telegram import Bot, Message + from telegram import Message class ChecklistTask(TelegramObject): @@ -112,25 +110,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChecklistTask": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - if (date := data.get("completion_date")) == 0: - data["completion_date"] = ZERO_DATE - else: - data["completion_date"] = from_timestamp(date, tzinfo=loc_tzinfo) - - data["completed_by_user"] = de_json_optional(data.get("completed_by_user"), User, bot) - data["completed_by_chat"] = de_json_optional(data.get("completed_by_chat"), Chat, bot) - data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot) - - return super().de_json(data=data, bot=bot) - def parse_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`text` from a given :class:`telegram.MessageEntity` of :attr:`text_entities`. @@ -231,16 +210,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Checklist": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["title_entities"] = de_list_optional(data.get("title_entities"), MessageEntity, bot) - data["tasks"] = de_list_optional(data.get("tasks"), ChecklistTask, bot) - - return super().de_json(data=data, bot=bot) - def parse_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`title` from a given :class:`telegram.MessageEntity` of :attr:`title_entities`. @@ -321,7 +290,7 @@ class ChecklistTasksDone(TelegramObject): def __init__( self, - checklist_message: Optional["Message"] = None, + checklist_message: "Message | None" = None, marked_as_done_task_ids: Sequence[int] | None = None, marked_as_not_done_task_ids: Sequence[int] | None = None, *, @@ -338,18 +307,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChecklistTasksDone": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # needs to be imported here to avoid circular import issues - from telegram import Message # pylint: disable=import-outside-toplevel # noqa: PLC0415 - - data["checklist_message"] = de_json_optional(data.get("checklist_message"), Message, bot) - - return super().de_json(data=data, bot=bot) - class ChecklistTasksAdded(TelegramObject): """ @@ -380,7 +337,7 @@ class ChecklistTasksAdded(TelegramObject): def __init__( self, tasks: Sequence[ChecklistTask], - checklist_message: Optional["Message"] = None, + checklist_message: "Message | None" = None, *, api_kwargs: JSONDict | None = None, ): @@ -391,16 +348,3 @@ def __init__( self._id_attrs = (self.tasks,) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChecklistTasksAdded": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # needs to be imported here to avoid circular import issues - from telegram import Message # pylint: disable=import-outside-toplevel # noqa: PLC0415 - - data["checklist_message"] = de_json_optional(data.get("checklist_message"), Message, bot) - data["tasks"] = ChecklistTask.de_list(data.get("tasks", []), bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_choseninlineresult.py b/src/telegram/_choseninlineresult.py index 41022e066da..8e8d26f7a55 100644 --- a/src/telegram/_choseninlineresult.py +++ b/src/telegram/_choseninlineresult.py @@ -24,7 +24,6 @@ from telegram._files.location import Location from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -91,15 +90,3 @@ def __init__( self._id_attrs = (self.result_id,) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChosenInlineResult": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Required - data["from_user"] = de_json_optional(data.pop("from", None), User, bot) - # Optionals - data["location"] = de_json_optional(data.get("location"), Location, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_directmessagestopic.py b/src/telegram/_directmessagestopic.py index 384c4db71c8..11e05fa1e22 100644 --- a/src/telegram/_directmessagestopic.py +++ b/src/telegram/_directmessagestopic.py @@ -22,7 +22,6 @@ from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -75,12 +74,3 @@ def __init__( self._id_attrs = (self.topic_id, self.user) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "DirectMessagesTopic": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["user"] = de_json_optional(data.get("user"), User, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_files/_basemedium.py b/src/telegram/_files/_basemedium.py index a6feb4325f3..5d9091d7248 100644 --- a/src/telegram/_files/_basemedium.py +++ b/src/telegram/_files/_basemedium.py @@ -48,7 +48,6 @@ class _BaseMedium(TelegramObject): Can't be used to download or reuse the file. file_size (:obj:`int`): Optional. File size. - """ __slots__ = ("file_id", "file_size", "file_unique_id") diff --git a/src/telegram/_files/_basethumbedmedium.py b/src/telegram/_files/_basethumbedmedium.py index bf03f37b3c0..96d17a8079c 100644 --- a/src/telegram/_files/_basethumbedmedium.py +++ b/src/telegram/_files/_basethumbedmedium.py @@ -18,16 +18,12 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """Common base class for media objects with thumbnails""" -from typing import TYPE_CHECKING, TypeVar +from typing import TypeVar from telegram._files._basemedium import _BaseMedium from telegram._files.photosize import PhotoSize -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - # pylint: disable=invalid-name ThumbedMT_co = TypeVar("ThumbedMT_co", bound="_BaseThumbedMedium", covariant=True) @@ -63,6 +59,11 @@ class _BaseThumbedMedium(_BaseMedium): """ __slots__ = ("thumbnail",) + __REMOVED_API_FIELDS__ = frozenset( + { + "thumb", + } + ) def __init__( self, @@ -81,20 +82,3 @@ def __init__( ) self.thumbnail: PhotoSize | None = thumbnail - - @classmethod - def de_json(cls: type[ThumbedMT_co], data: JSONDict, bot: "Bot | None" = None) -> ThumbedMT_co: - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # In case this wasn't already done by the subclass - if not isinstance(data.get("thumbnail"), PhotoSize): - data["thumbnail"] = de_json_optional(data.get("thumbnail"), PhotoSize, bot) - - api_kwargs = {} - # This is a deprecated field that TG still returns for backwards compatibility - # Let's filter it out to speed up the de-json process - if data.get("thumb") is not None: - api_kwargs["thumb"] = data.pop("thumb") - - return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) diff --git a/src/telegram/_files/sticker.py b/src/telegram/_files/sticker.py index e7854b53ab9..84abf43e40b 100644 --- a/src/telegram/_files/sticker.py +++ b/src/telegram/_files/sticker.py @@ -144,6 +144,11 @@ class Sticker(_BaseThumbedMedium): "type", "width", ) + __REMOVED_API_FIELDS__ = frozenset( + { + "thumb", + } + ) def __init__( self, @@ -194,23 +199,6 @@ def __init__( CUSTOM_EMOJI: Final[str] = constants.StickerType.CUSTOM_EMOJI """:const:`telegram.constants.StickerType.CUSTOM_EMOJI`""" - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Sticker": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["thumbnail"] = de_json_optional(data.get("thumbnail"), PhotoSize, bot) - data["mask_position"] = de_json_optional(data.get("mask_position"), MaskPosition, bot) - data["premium_animation"] = de_json_optional(data.get("premium_animation"), File, bot) - - api_kwargs = {} - # This is a deprecated field that TG still returns for backwards compatibility - # Let's filter it out to speed up the de-json process - if data.get("thumb") is not None: - api_kwargs["thumb"] = data.pop("thumb") - - return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) - class StickerSet(TelegramObject): """This object represents a sticker set. @@ -281,6 +269,7 @@ class StickerSet(TelegramObject): "thumbnail", "title", ) + __REMOVED_API_FIELDS__ = frozenset({"contains_masks", "is_animated", "is_video", "thumb"}) def __init__( self, @@ -303,23 +292,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "StickerSet": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["thumbnail"] = de_json_optional(data.get("thumbnail"), PhotoSize, bot) - data["stickers"] = de_list_optional(data.get("stickers"), Sticker, bot) - - api_kwargs = {} - # These are deprecated fields that TG still returns for backwards compatibility - # Let's filter them out to speed up the de-json process - for deprecated_field in ("contains_masks", "thumb", "is_animated", "is_video"): - if deprecated_field in data: - api_kwargs[deprecated_field] = data.pop(deprecated_field) - - return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) - class MaskPosition(TelegramObject): """This object describes the position on faces where a mask should be placed by default. diff --git a/src/telegram/_files/venue.py b/src/telegram/_files/venue.py index 3594e91e10a..f9ee3db9ae2 100644 --- a/src/telegram/_files/venue.py +++ b/src/telegram/_files/venue.py @@ -22,7 +22,6 @@ from telegram._files.location import Location from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -102,12 +101,3 @@ def __init__( self._id_attrs = (self.location, self.title) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Venue": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["location"] = de_json_optional(data.get("location"), Location, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_files/video.py b/src/telegram/_files/video.py index d197e053cd2..e07068794a8 100644 --- a/src/telegram/_files/video.py +++ b/src/telegram/_files/video.py @@ -29,9 +29,6 @@ from telegram._utils.datetime import get_timedelta_value from telegram._utils.types import JSONDict, TimePeriod -if TYPE_CHECKING: - from telegram import Bot - class Video(_BaseThumbedMedium): """This object represents a video file. @@ -167,13 +164,3 @@ def duration(self) -> int | dtm.timedelta: @property def start_timestamp(self) -> dtm.timedelta | None | int: return get_timedelta_value(self._start_timestamp, attribute="start_timestamp") - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Video": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["cover"] = de_list_optional(data.get("cover"), PhotoSize, bot) - data["qualities"] = de_list_optional(data.get("qualities"), VideoQuality, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_games/game.py b/src/telegram/_games/game.py index 898838e8b97..b1b6fc0254b 100644 --- a/src/telegram/_games/game.py +++ b/src/telegram/_games/game.py @@ -124,17 +124,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Game": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot) - data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot) - data["animation"] = de_json_optional(data.get("animation"), Animation, bot) - - return super().de_json(data=data, bot=bot) - def parse_text_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. diff --git a/src/telegram/_games/gamehighscore.py b/src/telegram/_games/gamehighscore.py index 482a61c9659..e0159b9ea97 100644 --- a/src/telegram/_games/gamehighscore.py +++ b/src/telegram/_games/gamehighscore.py @@ -22,7 +22,6 @@ from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -60,12 +59,3 @@ def __init__( self._id_attrs = (self.position, self.user, self.score) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "GameHighScore": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["user"] = de_json_optional(data.get("user"), User, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_gifts.py b/src/telegram/_gifts.py index 84a9bad7100..bb988f86777 100644 --- a/src/telegram/_gifts.py +++ b/src/telegram/_gifts.py @@ -228,16 +228,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Gift": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot) - data["publisher_chat"] = de_json_optional(data.get("publisher_chat"), Chat, bot) - data["background"] = de_json_optional(data.get("background"), GiftBackground, bot) - return super().de_json(data=data, bot=bot) - class Gifts(TelegramObject): """This object represent a list of gifts. @@ -270,14 +260,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Gifts": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["gifts"] = de_list_optional(data.get("gifts"), Gift, bot) - return super().de_json(data=data, bot=bot) - class GiftInfo(TelegramObject): """Describes a service message about a regular gift that was sent or received. @@ -385,16 +367,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "GiftInfo": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["gift"] = de_json_optional(data.get("gift"), Gift, bot) - data["entities"] = de_list_optional(data.get("entities"), MessageEntity, bot) - - return super().de_json(data=data, bot=bot) - def parse_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`text` from a given :class:`telegram.MessageEntity` of :attr:`entities`. diff --git a/src/telegram/_giveaway.py b/src/telegram/_giveaway.py index 3fe25c40920..d44a4eb04c3 100644 --- a/src/telegram/_giveaway.py +++ b/src/telegram/_giveaway.py @@ -25,12 +25,11 @@ from telegram._chat import Chat from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: - from telegram import Bot, Message + from telegram import Message class Giveaway(TelegramObject): @@ -137,21 +136,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Giveaway": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["chats"] = de_list_optional(data.get("chats"), Chat, bot) - data["winners_selection_date"] = from_timestamp( - data.get("winners_selection_date"), tzinfo=loc_tzinfo - ) - - return super().de_json(data=data, bot=bot) - class GiveawayCreated(TelegramObject): """This object represents a service message about the creation of a scheduled giveaway. @@ -292,22 +276,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "GiveawayWinners": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - data["winners"] = de_list_optional(data.get("winners"), User, bot) - data["winners_selection_date"] = from_timestamp( - data.get("winners_selection_date"), tzinfo=loc_tzinfo - ) - - return super().de_json(data=data, bot=bot) - class GiveawayCompleted(TelegramObject): """This object represents a service message about the completion of a giveaway without public @@ -363,17 +331,3 @@ def __init__( ) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "GiveawayCompleted": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Unfortunately, this needs to be here due to cyclic imports - from telegram._message import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415 - Message, - ) - - data["giveaway_message"] = de_json_optional(data.get("giveaway_message"), Message, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_inline/inlinekeyboardbutton.py b/src/telegram/_inline/inlinekeyboardbutton.py index 7915ac0aa8e..6d73d76fdbb 100644 --- a/src/telegram/_inline/inlinekeyboardbutton.py +++ b/src/telegram/_inline/inlinekeyboardbutton.py @@ -26,7 +26,6 @@ from telegram._loginurl import LoginUrl from telegram._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict from telegram._webappinfo import WebAppInfo @@ -344,21 +343,6 @@ def _set_id_attrs(self) -> None: self.icon_custom_emoji_id, ) - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "InlineKeyboardButton": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["login_url"] = de_json_optional(data.get("login_url"), LoginUrl, bot) - data["web_app"] = de_json_optional(data.get("web_app"), WebAppInfo, bot) - data["callback_game"] = de_json_optional(data.get("callback_game"), CallbackGame, bot) - data["switch_inline_query_chosen_chat"] = de_json_optional( - data.get("switch_inline_query_chosen_chat"), SwitchInlineQueryChosenChat, bot - ) - data["copy_text"] = de_json_optional(data.get("copy_text"), CopyTextButton, bot) - - return super().de_json(data=data, bot=bot) - def update_callback_data(self, callback_data: str | object) -> None: """ Sets :attr:`callback_data` to the passed object. Intended to be used by diff --git a/src/telegram/_inline/inlinekeyboardmarkup.py b/src/telegram/_inline/inlinekeyboardmarkup.py index 37040bc9ec5..166bdbd64bc 100644 --- a/src/telegram/_inline/inlinekeyboardmarkup.py +++ b/src/telegram/_inline/inlinekeyboardmarkup.py @@ -91,6 +91,7 @@ def __init__( self._freeze() + # TODO: Come back to this and modify TGObject to parse nested Sequence's @classmethod def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "InlineKeyboardMarkup": """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/src/telegram/_inline/inlinequery.py b/src/telegram/_inline/inlinequery.py index 48ff6162f17..eb83840826b 100644 --- a/src/telegram/_inline/inlinequery.py +++ b/src/telegram/_inline/inlinequery.py @@ -27,7 +27,6 @@ from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput, TimePeriod @@ -132,16 +131,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "InlineQuery": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["from_user"] = de_json_optional(data.pop("from", None), User, bot) - data["location"] = de_json_optional(data.get("location"), Location, bot) - - return super().de_json(data=data, bot=bot) - async def answer( self, results: ( diff --git a/src/telegram/_inline/inlinequeryresultsbutton.py b/src/telegram/_inline/inlinequeryresultsbutton.py index 7f124fe7fc8..5f06f858f3c 100644 --- a/src/telegram/_inline/inlinequeryresultsbutton.py +++ b/src/telegram/_inline/inlinequeryresultsbutton.py @@ -22,7 +22,6 @@ from telegram import constants from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict from telegram._webappinfo import WebAppInfo @@ -98,14 +97,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "InlineQueryResultsButton": - """See :meth:`telegram.TelegramObject.de_json`.""" - - data["web_app"] = de_json_optional(data.get("web_app"), WebAppInfo, bot) - - return super().de_json(data=data, bot=bot) - MIN_START_PARAMETER_LENGTH: Final[int] = ( constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH ) diff --git a/src/telegram/_inline/inputinvoicemessagecontent.py b/src/telegram/_inline/inputinvoicemessagecontent.py index 536726e3afa..87638bf405a 100644 --- a/src/telegram/_inline/inputinvoicemessagecontent.py +++ b/src/telegram/_inline/inputinvoicemessagecontent.py @@ -254,12 +254,3 @@ def __init__( self.currency, self.prices, ) - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "InputInvoiceMessageContent": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["prices"] = de_list_optional(data.get("prices"), LabeledPrice, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_inline/preparedinlinemessage.py b/src/telegram/_inline/preparedinlinemessage.py index b390c228e3e..73e858a95c5 100644 --- a/src/telegram/_inline/preparedinlinemessage.py +++ b/src/telegram/_inline/preparedinlinemessage.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING from telegram._telegramobject import TelegramObject -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -66,14 +65,3 @@ def __init__( self._id_attrs = (self.id,) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PreparedInlineMessage": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["expiration_date"] = from_timestamp(data.get("expiration_date"), tzinfo=loc_tzinfo) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_keyboardbutton.py b/src/telegram/_keyboardbutton.py index 750062bf516..109b3ca0414 100644 --- a/src/telegram/_keyboardbutton.py +++ b/src/telegram/_keyboardbutton.py @@ -181,6 +181,12 @@ class KeyboardButton(TelegramObject): "web_app", ) + __REMOVED_API_FIELDS__ = frozenset( + { + "request_user", + } + ) + def __init__( self, text: str, @@ -222,27 +228,3 @@ def __init__( ) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "KeyboardButton": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["request_poll"] = de_json_optional( - data.get("request_poll"), KeyboardButtonPollType, bot - ) - data["request_users"] = de_json_optional( - data.get("request_users"), KeyboardButtonRequestUsers, bot - ) - data["request_chat"] = de_json_optional( - data.get("request_chat"), KeyboardButtonRequestChat, bot - ) - data["web_app"] = de_json_optional(data.get("web_app"), WebAppInfo, bot) - - api_kwargs = {} - # This is a deprecated field that TG still returns for backwards compatibility - # Let's filter it out to speed up the de-json process - if request_user := data.get("request_user"): - api_kwargs = {"request_user": request_user} - - return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) diff --git a/src/telegram/_keyboardbuttonrequest.py b/src/telegram/_keyboardbuttonrequest.py index b5e0b050436..457a4ca015b 100644 --- a/src/telegram/_keyboardbuttonrequest.py +++ b/src/telegram/_keyboardbuttonrequest.py @@ -254,17 +254,3 @@ def __init__( self._id_attrs = (self.request_id,) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "KeyboardButtonRequestChat": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["user_administrator_rights"] = de_json_optional( - data.get("user_administrator_rights"), ChatAdministratorRights, bot - ) - data["bot_administrator_rights"] = de_json_optional( - data.get("bot_administrator_rights"), ChatAdministratorRights, bot - ) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_menubutton.py b/src/telegram/_menubutton.py index e570c5a6648..3788f453949 100644 --- a/src/telegram/_menubutton.py +++ b/src/telegram/_menubutton.py @@ -23,7 +23,6 @@ from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict from telegram._webappinfo import WebAppInfo @@ -165,15 +164,6 @@ def __init__(self, text: str, web_app: WebAppInfo, *, api_kwargs: JSONDict | Non self._id_attrs = (self.type, self.text, self.web_app) - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "MenuButtonWebApp": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["web_app"] = de_json_optional(data.get("web_app"), WebAppInfo, bot) - - return super().de_json(data=data, bot=bot) # type: ignore[return-value] - class MenuButtonDefault(MenuButton): """Describes that no specific value for the menu button was set. diff --git a/src/telegram/_messageentity.py b/src/telegram/_messageentity.py index 7eebf30c848..bf46ddcbc1f 100644 --- a/src/telegram/_messageentity.py +++ b/src/telegram/_messageentity.py @@ -28,8 +28,6 @@ from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.strings import TextEncoding from telegram._utils.types import JSONDict @@ -183,19 +181,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "MessageEntity": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["user"] = de_json_optional(data.get("user"), User, bot) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["unix_time"] = from_timestamp(data.get("unix_time"), tzinfo=loc_tzinfo) - - return super().de_json(data=data, bot=bot) - @staticmethod def adjust_message_entities_to_utf_16(text: str, entities: _SEM) -> _SEM: """Utility functionality for converting the offset and length of entities from diff --git a/src/telegram/_messagereactionupdated.py b/src/telegram/_messagereactionupdated.py index d5e5471954f..d2dd3293777 100644 --- a/src/telegram/_messagereactionupdated.py +++ b/src/telegram/_messagereactionupdated.py @@ -27,7 +27,6 @@ from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -86,20 +85,6 @@ def __init__( self._id_attrs = (self.chat, self.message_id, self.date, self.reactions) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "MessageReactionCountUpdated": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo) - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - data["reactions"] = de_list_optional(data.get("reactions"), ReactionCount, bot) - - return super().de_json(data=data, bot=bot) - class MessageReactionUpdated(TelegramObject): """This class represents a change of a reaction on a message performed by a user. @@ -181,20 +166,3 @@ def __init__( self.new_reaction, ) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "MessageReactionUpdated": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo) - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - data["old_reaction"] = de_list_optional(data.get("old_reaction"), ReactionType, bot) - data["new_reaction"] = de_list_optional(data.get("new_reaction"), ReactionType, bot) - data["user"] = de_json_optional(data.get("user"), User, bot) - data["actor_chat"] = de_json_optional(data.get("actor_chat"), Chat, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_ownedgift.py b/src/telegram/_ownedgift.py index 37ae8f693a6..b142a26cf8a 100644 --- a/src/telegram/_ownedgift.py +++ b/src/telegram/_ownedgift.py @@ -30,7 +30,6 @@ from telegram._user import User from telegram._utils import enum from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.entities import parse_message_entities, parse_message_entity from telegram._utils.types import JSONDict @@ -286,19 +285,6 @@ def __init__( self._id_attrs = (self.type, self.gift, self.send_date) - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "OwnedGiftRegular": - """See :meth:`telegram.OwnedGift.de_json`.""" - data = cls._parse_data(data) - - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) - data["sender_user"] = de_json_optional(data.get("sender_user"), User, bot) - data["gift"] = de_json_optional(data.get("gift"), Gift, bot) - data["entities"] = de_list_optional(data.get("entities"), MessageEntity, bot) - - return super().de_json(data=data, bot=bot) # type: ignore[return-value] - def parse_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`text` from a given :class:`telegram.MessageEntity` of :attr:`entities`. @@ -438,18 +424,3 @@ def __init__( self.next_transfer_date: dtm.datetime | None = next_transfer_date self._id_attrs = (self.type, self.gift, self.send_date) - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "OwnedGiftUnique": - """See :meth:`telegram.OwnedGift.de_json`.""" - data = cls._parse_data(data) - - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) - data["sender_user"] = de_json_optional(data.get("sender_user"), User, bot) - data["gift"] = de_json_optional(data.get("gift"), UniqueGift, bot) - data["next_transfer_date"] = from_timestamp( - data.get("next_transfer_date"), tzinfo=loc_tzinfo - ) - - return super().de_json(data=data, bot=bot) # type: ignore[return-value] diff --git a/src/telegram/_paidmedia.py b/src/telegram/_paidmedia.py index 67af46710a5..2353f4910d2 100644 --- a/src/telegram/_paidmedia.py +++ b/src/telegram/_paidmedia.py @@ -29,8 +29,6 @@ from telegram._user import User from telegram._utils import enum from telegram._utils.argumentparsing import ( - de_json_optional, - de_list_optional, parse_sequence_arg, to_timedelta, ) @@ -202,13 +200,6 @@ def __init__( self._id_attrs = (self.type, self.photo) - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PaidMediaPhoto": - data = cls._parse_data(data) - - data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot) - return super().de_json(data=data, bot=bot) # type: ignore[return-value] - class PaidMediaVideo(PaidMedia): """ @@ -243,13 +234,6 @@ def __init__( self._id_attrs = (self.type, self.video) - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PaidMediaVideo": - data = cls._parse_data(data) - - data["video"] = de_json_optional(data.get("video"), Video, bot) - return super().de_json(data=data, bot=bot) # type: ignore[return-value] - class PaidMediaInfo(TelegramObject): """ @@ -287,13 +271,6 @@ def __init__( self._id_attrs = (self.star_count, self.paid_media) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PaidMediaInfo": - data = cls._parse_data(data) - - data["paid_media"] = de_list_optional(data.get("paid_media"), PaidMedia, bot) - return super().de_json(data=data, bot=bot) - class PaidMediaPurchased(TelegramObject): """This object contains information about a paid media purchase. @@ -330,10 +307,3 @@ def __init__( self._id_attrs = (self.from_user, self.paid_media_payload) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PaidMediaPurchased": - data = cls._parse_data(data) - - data["from_user"] = User.de_json(data=data.pop("from"), bot=bot) - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_passport/credentials.py b/src/telegram/_passport/credentials.py index d5a16443980..ff754a6b270 100644 --- a/src/telegram/_passport/credentials.py +++ b/src/telegram/_passport/credentials.py @@ -233,15 +233,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Credentials": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["secure_data"] = de_json_optional(data.get("secure_data"), SecureData, bot) - - return super().de_json(data=data, bot=bot) - class SecureData(TelegramObject): """ @@ -340,31 +331,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "SecureData": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["temporary_registration"] = de_json_optional( - data.get("temporary_registration"), SecureValue, bot - ) - data["passport_registration"] = de_json_optional( - data.get("passport_registration"), SecureValue, bot - ) - data["rental_agreement"] = de_json_optional(data.get("rental_agreement"), SecureValue, bot) - data["bank_statement"] = de_json_optional(data.get("bank_statement"), SecureValue, bot) - data["utility_bill"] = de_json_optional(data.get("utility_bill"), SecureValue, bot) - data["address"] = de_json_optional(data.get("address"), SecureValue, bot) - data["identity_card"] = de_json_optional(data.get("identity_card"), SecureValue, bot) - data["driver_license"] = de_json_optional(data.get("driver_license"), SecureValue, bot) - data["internal_passport"] = de_json_optional( - data.get("internal_passport"), SecureValue, bot - ) - data["passport"] = de_json_optional(data.get("passport"), SecureValue, bot) - data["personal_details"] = de_json_optional(data.get("personal_details"), SecureValue, bot) - - return super().de_json(data=data, bot=bot) - class SecureValue(TelegramObject): """ @@ -445,20 +411,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "SecureValue": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["data"] = de_json_optional(data.get("data"), DataCredentials, bot) - data["front_side"] = de_json_optional(data.get("front_side"), FileCredentials, bot) - data["reverse_side"] = de_json_optional(data.get("reverse_side"), FileCredentials, bot) - data["selfie"] = de_json_optional(data.get("selfie"), FileCredentials, bot) - data["files"] = de_list_optional(data.get("files"), FileCredentials, bot) - data["translation"] = de_list_optional(data.get("translation"), FileCredentials, bot) - - return super().de_json(data=data, bot=bot) - class _CredentialsBase(TelegramObject): """Base class for DataCredentials and FileCredentials.""" diff --git a/src/telegram/_passport/encryptedpassportelement.py b/src/telegram/_passport/encryptedpassportelement.py index 8b4bdfbd77e..c8669cb036a 100644 --- a/src/telegram/_passport/encryptedpassportelement.py +++ b/src/telegram/_passport/encryptedpassportelement.py @@ -27,9 +27,7 @@ from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import ( de_json_decrypted_optional, - de_json_optional, de_list_decrypted_optional, - de_list_optional, parse_sequence_arg, ) from telegram._utils.types import JSONDict @@ -195,19 +193,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "EncryptedPassportElement": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["files"] = de_list_optional(data.get("files"), PassportFile, bot) or None - data["front_side"] = de_json_optional(data.get("front_side"), PassportFile, bot) - data["reverse_side"] = de_json_optional(data.get("reverse_side"), PassportFile, bot) - data["selfie"] = de_json_optional(data.get("selfie"), PassportFile, bot) - data["translation"] = de_list_optional(data.get("translation"), PassportFile, bot) or None - - return super().de_json(data=data, bot=bot) - @classmethod def de_json_decrypted( cls, data: JSONDict, bot: "Bot | None", credentials: "Credentials" diff --git a/src/telegram/_passport/passportdata.py b/src/telegram/_passport/passportdata.py index deae7196f3a..76088709dbb 100644 --- a/src/telegram/_passport/passportdata.py +++ b/src/telegram/_passport/passportdata.py @@ -82,16 +82,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PassportData": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["data"] = de_list_optional(data.get("data"), EncryptedPassportElement, bot) - data["credentials"] = de_json_optional(data.get("credentials"), EncryptedCredentials, bot) - - return super().de_json(data=data, bot=bot) - @property def decrypted_data(self) -> tuple[EncryptedPassportElement, ...]: """ diff --git a/src/telegram/_passport/passportfile.py b/src/telegram/_passport/passportfile.py index 724fb51ed1c..fd4c8a8189c 100644 --- a/src/telegram/_passport/passportfile.py +++ b/src/telegram/_passport/passportfile.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING from telegram._telegramobject import TelegramObject -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 @@ -98,17 +97,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PassportFile": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["file_date"] = from_timestamp(data.get("file_date"), tzinfo=loc_tzinfo) - - return super().de_json(data=data, bot=bot) - @classmethod def de_json_decrypted( cls, data: JSONDict, bot: "Bot | None", credentials: "FileCredentials" diff --git a/src/telegram/_payment/orderinfo.py b/src/telegram/_payment/orderinfo.py index c3f8a798d80..fd73e67704b 100644 --- a/src/telegram/_payment/orderinfo.py +++ b/src/telegram/_payment/orderinfo.py @@ -22,7 +22,6 @@ from telegram._payment.shippingaddress import ShippingAddress from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -70,14 +69,3 @@ def __init__( self._id_attrs = (self.name, self.phone_number, self.email, self.shipping_address) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "OrderInfo": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["shipping_address"] = de_json_optional( - data.get("shipping_address"), ShippingAddress, bot - ) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_payment/precheckoutquery.py b/src/telegram/_payment/precheckoutquery.py index 94acae703cb..edb1f2ab413 100644 --- a/src/telegram/_payment/precheckoutquery.py +++ b/src/telegram/_payment/precheckoutquery.py @@ -23,7 +23,6 @@ from telegram._payment.orderinfo import OrderInfo from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput @@ -110,16 +109,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PreCheckoutQuery": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["from_user"] = de_json_optional(data.pop("from", None), User, bot) - data["order_info"] = de_json_optional(data.get("order_info"), OrderInfo, bot) - - return super().de_json(data=data, bot=bot) - async def answer( self, ok: bool, diff --git a/src/telegram/_payment/shippingquery.py b/src/telegram/_payment/shippingquery.py index 2b3f92b3a16..c5f34ec4ab3 100644 --- a/src/telegram/_payment/shippingquery.py +++ b/src/telegram/_payment/shippingquery.py @@ -24,7 +24,6 @@ from telegram._payment.shippingaddress import ShippingAddress from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput @@ -78,18 +77,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ShippingQuery": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["from_user"] = de_json_optional(data.pop("from", None), User, bot) - data["shipping_address"] = de_json_optional( - data.get("shipping_address"), ShippingAddress, bot - ) - - return super().de_json(data=data, bot=bot) - async def answer( self, ok: bool, diff --git a/src/telegram/_payment/stars/affiliateinfo.py b/src/telegram/_payment/stars/affiliateinfo.py index 5b178f74483..99fb6ec6205 100644 --- a/src/telegram/_payment/stars/affiliateinfo.py +++ b/src/telegram/_payment/stars/affiliateinfo.py @@ -23,7 +23,6 @@ from telegram._chat import Chat from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -105,13 +104,3 @@ def __init__( self.nanostar_amount, ) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "AffiliateInfo": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["affiliate_user"] = de_json_optional(data.get("affiliate_user"), User, bot) - data["affiliate_chat"] = de_json_optional(data.get("affiliate_chat"), Chat, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_payment/stars/revenuewithdrawalstate.py b/src/telegram/_payment/stars/revenuewithdrawalstate.py index beb0e7de74f..2fcea224a01 100644 --- a/src/telegram/_payment/stars/revenuewithdrawalstate.py +++ b/src/telegram/_payment/stars/revenuewithdrawalstate.py @@ -25,7 +25,6 @@ from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -150,19 +149,6 @@ def __init__( self.date, ) - @classmethod - def de_json( - cls, data: JSONDict, bot: "Bot | None" = None - ) -> "RevenueWithdrawalStateSucceeded": - """See :meth:`telegram.RevenueWithdrawalState.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo) - - return super().de_json(data=data, bot=bot) # type: ignore[return-value] - class RevenueWithdrawalStateFailed(RevenueWithdrawalState): """The withdrawal failed and the transaction was refunded. diff --git a/src/telegram/_payment/stars/startransactions.py b/src/telegram/_payment/stars/startransactions.py index b8502fd8fd7..36f60a34285 100644 --- a/src/telegram/_payment/stars/startransactions.py +++ b/src/telegram/_payment/stars/startransactions.py @@ -25,7 +25,6 @@ from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict from .transactionpartner import TransactionPartner @@ -114,20 +113,6 @@ def __init__( ) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "StarTransaction": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo) - - data["source"] = de_json_optional(data.get("source"), TransactionPartner, bot) - data["receiver"] = de_json_optional(data.get("receiver"), TransactionPartner, bot) - - return super().de_json(data=data, bot=bot) - class StarTransactions(TelegramObject): """ @@ -155,11 +140,3 @@ def __init__( self._id_attrs = (self.transactions,) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "StarTransactions": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["transactions"] = de_list_optional(data.get("transactions"), StarTransaction, bot) - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_payment/stars/transactionpartner.py b/src/telegram/_payment/stars/transactionpartner.py index 09d96c159da..407447d2f9a 100644 --- a/src/telegram/_payment/stars/transactionpartner.py +++ b/src/telegram/_payment/stars/transactionpartner.py @@ -30,10 +30,10 @@ from telegram._user import User from telegram._utils import enum from telegram._utils.argumentparsing import ( - de_json_optional, - de_list_optional, parse_sequence_arg, to_timedelta, + de_json_optional, + de_list_optional, ) from telegram._utils.types import JSONDict, TimePeriod diff --git a/src/telegram/_payment/successfulpayment.py b/src/telegram/_payment/successfulpayment.py index 7fb284814c6..fad4e6eed1e 100644 --- a/src/telegram/_payment/successfulpayment.py +++ b/src/telegram/_payment/successfulpayment.py @@ -23,8 +23,6 @@ from telegram._payment.orderinfo import OrderInfo from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -140,19 +138,3 @@ def __init__( self._id_attrs = (self.telegram_payment_charge_id, self.provider_payment_charge_id) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "SuccessfulPayment": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["order_info"] = de_json_optional(data.get("order_info"), OrderInfo, bot) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["subscription_expiration_date"] = from_timestamp( - data.get("subscription_expiration_date"), tzinfo=loc_tzinfo - ) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_poll.py b/src/telegram/_poll.py index 744edd22eff..99ea05d96e7 100644 --- a/src/telegram/_poll.py +++ b/src/telegram/_poll.py @@ -29,16 +29,11 @@ from telegram._user import User from telegram._utils import enum from telegram._utils.argumentparsing import ( - de_json_optional, de_list_optional, parse_sequence_arg, to_timedelta, ) -from telegram._utils.datetime import ( - extract_tzinfo_from_defaults, - from_timestamp, - get_timedelta_value, -) +from telegram._utils.datetime import get_timedelta_value from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.entities import parse_message_entities, parse_message_entity from telegram._utils.types import JSONDict, ODVInput, TimePeriod @@ -100,15 +95,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "InputPollOption": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot) - - return super().de_json(data=data, bot=bot) - class PollOption(TelegramObject): """ @@ -161,15 +147,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PollOption": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot) - - return super().de_json(data=data, bot=bot) - def parse_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`text` from a given :class:`telegram.MessageEntity` of :attr:`text_entities`. @@ -305,16 +282,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PollAnswer": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["user"] = de_json_optional(data.get("user"), User, bot) - data["voter_chat"] = de_json_optional(data.get("voter_chat"), Chat, bot) - - return super().de_json(data=data, bot=bot) - class Poll(TelegramObject): """ @@ -478,25 +445,6 @@ def __init__( def open_period(self) -> int | dtm.timedelta | None: return get_timedelta_value(self._open_period, attribute="open_period") - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Poll": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["options"] = de_list_optional(data.get("options"), PollOption, bot) - data["explanation_entities"] = de_list_optional( - data.get("explanation_entities"), MessageEntity, bot - ) - data["close_date"] = from_timestamp(data.get("close_date"), tzinfo=loc_tzinfo) - data["question_entities"] = de_list_optional( - data.get("question_entities"), MessageEntity, bot - ) - - return super().de_json(data=data, bot=bot) - def parse_explanation_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`explanation` from a given :class:`telegram.MessageEntity` of :attr:`explanation_entities`. diff --git a/src/telegram/_proximityalerttriggered.py b/src/telegram/_proximityalerttriggered.py index 8f07d9aede4..2e9678f322f 100644 --- a/src/telegram/_proximityalerttriggered.py +++ b/src/telegram/_proximityalerttriggered.py @@ -22,7 +22,6 @@ from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -67,13 +66,3 @@ def __init__( self._id_attrs = (self.traveler, self.watcher, self.distance) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ProximityAlertTriggered": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["traveler"] = de_json_optional(data.get("traveler"), User, bot) - data["watcher"] = de_json_optional(data.get("watcher"), User, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_reaction.py b/src/telegram/_reaction.py index 2dbcd462e72..69670e5dceb 100644 --- a/src/telegram/_reaction.py +++ b/src/telegram/_reaction.py @@ -24,7 +24,6 @@ from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -220,12 +219,3 @@ def __init__( self.total_count, ) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ReactionCount": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["type"] = de_json_optional(data.get("type"), ReactionType, bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_reply.py b/src/telegram/_reply.py index 367d5aad7a0..010a304e46e 100644 --- a/src/telegram/_reply.py +++ b/src/telegram/_reply.py @@ -258,41 +258,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ExternalReplyInfo": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["origin"] = de_json_optional(data.get("origin"), MessageOrigin, bot) - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - data["link_preview_options"] = de_json_optional( - data.get("link_preview_options"), LinkPreviewOptions, bot - ) - data["animation"] = de_json_optional(data.get("animation"), Animation, bot) - data["audio"] = de_json_optional(data.get("audio"), Audio, bot) - data["document"] = de_json_optional(data.get("document"), Document, bot) - data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot) - data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot) - data["story"] = de_json_optional(data.get("story"), Story, bot) - data["video"] = de_json_optional(data.get("video"), Video, bot) - data["video_note"] = de_json_optional(data.get("video_note"), VideoNote, bot) - data["voice"] = de_json_optional(data.get("voice"), Voice, bot) - data["contact"] = de_json_optional(data.get("contact"), Contact, bot) - data["dice"] = de_json_optional(data.get("dice"), Dice, bot) - data["game"] = de_json_optional(data.get("game"), Game, bot) - data["giveaway"] = de_json_optional(data.get("giveaway"), Giveaway, bot) - data["giveaway_winners"] = de_json_optional( - data.get("giveaway_winners"), GiveawayWinners, bot - ) - data["invoice"] = de_json_optional(data.get("invoice"), Invoice, bot) - data["location"] = de_json_optional(data.get("location"), Location, bot) - data["poll"] = de_json_optional(data.get("poll"), Poll, bot) - data["venue"] = de_json_optional(data.get("venue"), Venue, bot) - data["paid_media"] = de_json_optional(data.get("paid_media"), PaidMediaInfo, bot) - data["checklist"] = de_json_optional(data.get("checklist"), Checklist, bot) - - return super().de_json(data=data, bot=bot) - class TextQuote(TelegramObject): """ @@ -358,15 +323,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "TextQuote": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["entities"] = de_list_optional(data.get("entities"), MessageEntity, bot) - - return super().de_json(data=data, bot=bot) - class ReplyParameters(TelegramObject): """ @@ -477,14 +433,3 @@ def __init__( self._id_attrs = (self.message_id,) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ReplyParameters": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["quote_entities"] = tuple( - de_list_optional(data.get("quote_entities"), MessageEntity, bot) - ) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_shared.py b/src/telegram/_shared.py index 2de07529c62..069aeb47a6e 100644 --- a/src/telegram/_shared.py +++ b/src/telegram/_shared.py @@ -70,6 +70,12 @@ class UsersShared(TelegramObject): __slots__ = ("request_id", "users") + __REMOVED_API_FIELDS__ = frozenset( + { + "user_ids", + } + ) + def __init__( self, request_id: int, @@ -85,21 +91,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "UsersShared": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["users"] = de_list_optional(data.get("users"), SharedUser, bot) - - api_kwargs = {} - # This is a deprecated field that TG still returns for backwards compatibility - # Let's filter it out to speed up the de-json process - if user_ids := data.get("user_ids"): - api_kwargs = {"user_ids": user_ids} - - return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) - class ChatShared(TelegramObject): """ @@ -171,14 +162,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatShared": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot) - return super().de_json(data=data, bot=bot) - @property def link(self) -> str | None: """:obj:`str`: Convenience property. If :attr:`username` is available, returns a t.me link @@ -281,11 +264,3 @@ def link(self) -> str | None: .. versionadded:: 22.4 """ return get_link(self) - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "SharedUser": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot) - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_story.py b/src/telegram/_story.py index 680e7206230..de6fbff5882 100644 --- a/src/telegram/_story.py +++ b/src/telegram/_story.py @@ -71,14 +71,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Story": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["chat"] = Chat.de_json(data.get("chat", {}), bot) - return super().de_json(data=data, bot=bot) - async def repost( self, business_connection_id: str, diff --git a/src/telegram/_suggestedpost.py b/src/telegram/_suggestedpost.py index b820706685a..5a49c75cd71 100644 --- a/src/telegram/_suggestedpost.py +++ b/src/telegram/_suggestedpost.py @@ -26,8 +26,6 @@ from telegram._payment.stars.staramount import StarAmount from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -141,20 +139,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostParameters": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["price"] = de_json_optional(data.get("price"), SuggestedPostPrice, bot) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) - - return super().de_json(data=data, bot=bot) - class SuggestedPostInfo(TelegramObject): """ @@ -223,19 +207,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostInfo": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["price"] = de_json_optional(data.get("price"), SuggestedPostPrice, bot) - data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) - - return super().de_json(data=data, bot=bot) - class SuggestedPostDeclined(TelegramObject): """ @@ -281,17 +252,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostDeclined": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["suggested_post_message"] = de_json_optional( - data.get("suggested_post_message"), Message, bot - ) - - return super().de_json(data=data, bot=bot) - class SuggestedPostPaid(TelegramObject): """ @@ -363,18 +323,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostPaid": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["suggested_post_message"] = de_json_optional( - data.get("suggested_post_message"), Message, bot - ) - data["star_amount"] = de_json_optional(data.get("star_amount"), StarAmount, bot) - - return super().de_json(data=data, bot=bot) - class SuggestedPostRefunded(TelegramObject): """ @@ -430,17 +378,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostRefunded": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["suggested_post_message"] = de_json_optional( - data.get("suggested_post_message"), Message, bot - ) - - return super().de_json(data=data, bot=bot) - class SuggestedPostApproved(TelegramObject): """ @@ -496,22 +433,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostApproved": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["send_date"] = from_timestamp(data.get("send_date"), tzinfo=loc_tzinfo) - data["price"] = de_json_optional(data.get("price"), SuggestedPostPrice, bot) - data["suggested_post_message"] = de_json_optional( - data.get("suggested_post_message"), Message, bot - ) - - return super().de_json(data=data, bot=bot) - class SuggestedPostApprovalFailed(TelegramObject): """ @@ -559,15 +480,3 @@ def __init__( self._id_attrs = (self.price, self.suggested_post_message) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "SuggestedPostApprovalFailed": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["price"] = de_json_optional(data.get("price"), SuggestedPostPrice, bot) - data["suggested_post_message"] = de_json_optional( - data.get("suggested_post_message"), Message, bot - ) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index 5a9559cde8e..791da3c048d 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -435,7 +435,7 @@ def _build_plan(cls) -> dict[str, Any]: """ init_fn = cls.__dict__.get("__init__") if init_fn is None: - # No own __init__: inherit the nearest ancestor's plan. This is true for e.g. _ChatBase + # No own __init__: inherit the nearest ancestor's plan. This is true for e.g. Chat parent = cls.__mro__[1] print("No __init__ for", cls.__name__, "inheriting from", parent.__name__) if "__DE_JSON_PLAN__" not in parent.__dict__: @@ -468,21 +468,25 @@ def _build_plan(cls) -> dict[str, Any]: else from_timestamp(value, tzinfo=extract_tzinfo_from_defaults(bot)) ) elif isinstance(inner, type) and issubclass(inner, TelegramObject): + print("Adding de_json plan for", cls.__name__, name, "→", inner.__name__) plan[name] = lambda v, b, _c=inner: _c.de_json(v, b) if isinstance(v, dict) else v elif origin is Sequence: args = get_args(inner) - # args: tuple[object, ...] = getattr(inner, "__args__", ()) if not args: continue item_type: object = args[0] - # if isinstance(item_type, str): - # print(f"Resolving forward reference for {cls.__name__}.{name}: {item_type}") - # with contextlib.suppress(Exception): - # item_type = eval(item_type, globalns, tg_ns) # noqa: S307 + # inspect.signature doesn't resolve the forward ref inside Sequence for some reason + if isinstance(item_type, str): + print(f"Resolving forward reference for {cls.__name__}.{name}: {item_type}") + # with contextlib.suppress(Exception): + item_type = eval(item_type, globalns, tg_ns) # noqa: S307 if isinstance(item_type, type) and issubclass(item_type, TelegramObject): + print("Adding de_list plan for", cls.__name__, name, "→", item_type.__name__) plan[name] = lambda v, b, _c=item_type: ( _c.de_list(v, b) if isinstance(v, list) else v ) + else: + print(f"No de_json plan for {cls.__name__}.{name} (annotation: {ann})") cls.__DE_JSON_PLAN__ = plan return plan diff --git a/src/telegram/_uniquegift.py b/src/telegram/_uniquegift.py index 33f35aae28e..f0a32b36c31 100644 --- a/src/telegram/_uniquegift.py +++ b/src/telegram/_uniquegift.py @@ -29,7 +29,6 @@ from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.argumentparsing import de_json_optional, parse_sequence_arg -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -173,15 +172,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "UniqueGiftModel": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot) - - return super().de_json(data=data, bot=bot) - class UniqueGiftSymbol(TelegramObject): """This object describes the symbol shown on the pattern of a unique gift. @@ -228,15 +218,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "UniqueGiftSymbol": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot) - - return super().de_json(data=data, bot=bot) - class UniqueGiftBackdropColors(TelegramObject): """This object describes the colors of the backdrop of a unique gift. @@ -333,15 +314,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "UniqueGiftBackdrop": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["colors"] = de_json_optional(data.get("colors"), UniqueGiftBackdropColors, bot) - - return super().de_json(data=data, bot=bot) - class UniqueGift(TelegramObject): """This object describes a unique gift that was upgraded from a regular gift. @@ -483,19 +455,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "UniqueGift": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["model"] = de_json_optional(data.get("model"), UniqueGiftModel, bot) - data["symbol"] = de_json_optional(data.get("symbol"), UniqueGiftSymbol, bot) - data["backdrop"] = de_json_optional(data.get("backdrop"), UniqueGiftBackdrop, bot) - data["publisher_chat"] = de_json_optional(data.get("publisher_chat"), Chat, bot) - data["colors"] = de_json_optional(data.get("colors"), UniqueGiftColors, bot) - - return super().de_json(data=data, bot=bot) - class UniqueGiftInfo(TelegramObject): """Describes a service message about a unique gift that was sent or received. @@ -627,16 +586,3 @@ def __init__( self._id_attrs = (self.gift, self.origin) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "UniqueGiftInfo": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["gift"] = de_json_optional(data.get("gift"), UniqueGift, bot) - data["next_transfer_date"] = from_timestamp( - data.get("next_transfer_date"), tzinfo=loc_tzinfo - ) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_update.py b/src/telegram/_update.py index 9e018e4cb0f..fe86792c646 100644 --- a/src/telegram/_update.py +++ b/src/telegram/_update.py @@ -35,7 +35,6 @@ from telegram._payment.shippingquery import ShippingQuery from telegram._poll import Poll, PollAnswer from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict from telegram._utils.warnings import warn @@ -754,58 +753,3 @@ def effective_message(self) -> Message | None: self._effective_message = message return message - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Update": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["message"] = de_json_optional(data.get("message"), Message, bot) - data["edited_message"] = de_json_optional(data.get("edited_message"), Message, bot) - data["inline_query"] = de_json_optional(data.get("inline_query"), InlineQuery, bot) - data["chosen_inline_result"] = de_json_optional( - data.get("chosen_inline_result"), ChosenInlineResult, bot - ) - data["callback_query"] = de_json_optional(data.get("callback_query"), CallbackQuery, bot) - data["shipping_query"] = de_json_optional(data.get("shipping_query"), ShippingQuery, bot) - data["pre_checkout_query"] = de_json_optional( - data.get("pre_checkout_query"), PreCheckoutQuery, bot - ) - data["channel_post"] = de_json_optional(data.get("channel_post"), Message, bot) - data["edited_channel_post"] = de_json_optional( - data.get("edited_channel_post"), Message, bot - ) - data["poll"] = de_json_optional(data.get("poll"), Poll, bot) - data["poll_answer"] = de_json_optional(data.get("poll_answer"), PollAnswer, bot) - data["my_chat_member"] = de_json_optional( - data.get("my_chat_member"), ChatMemberUpdated, bot - ) - data["chat_member"] = de_json_optional(data.get("chat_member"), ChatMemberUpdated, bot) - data["chat_join_request"] = de_json_optional( - data.get("chat_join_request"), ChatJoinRequest, bot - ) - data["chat_boost"] = de_json_optional(data.get("chat_boost"), ChatBoostUpdated, bot) - data["removed_chat_boost"] = de_json_optional( - data.get("removed_chat_boost"), ChatBoostRemoved, bot - ) - data["message_reaction"] = de_json_optional( - data.get("message_reaction"), MessageReactionUpdated, bot - ) - data["message_reaction_count"] = de_json_optional( - data.get("message_reaction_count"), MessageReactionCountUpdated, bot - ) - data["business_connection"] = de_json_optional( - data.get("business_connection"), BusinessConnection, bot - ) - data["business_message"] = de_json_optional(data.get("business_message"), Message, bot) - data["edited_business_message"] = de_json_optional( - data.get("edited_business_message"), Message, bot - ) - data["deleted_business_messages"] = de_json_optional( - data.get("deleted_business_messages"), BusinessMessagesDeleted, bot - ) - data["purchased_paid_media"] = de_json_optional( - data.get("purchased_paid_media"), PaidMediaPurchased, bot - ) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_userprofileaudios.py b/src/telegram/_userprofileaudios.py index 769298f0b83..b1b924a3033 100644 --- a/src/telegram/_userprofileaudios.py +++ b/src/telegram/_userprofileaudios.py @@ -65,12 +65,3 @@ def __init__( self._id_attrs = (self.total_count, self.audios) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "UserProfileAudios": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["audios"] = Audio.de_list(data.get("audios", []), bot) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_userprofilephotos.py b/src/telegram/_userprofilephotos.py index 59cebae1835..036378f4efa 100644 --- a/src/telegram/_userprofilephotos.py +++ b/src/telegram/_userprofilephotos.py @@ -70,12 +70,3 @@ def __init__( self._id_attrs = (self.total_count, self.photos) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "UserProfilePhotos": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["photos"] = [PhotoSize.de_list(photo, bot) for photo in data["photos"]] - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_videochat.py b/src/telegram/_videochat.py index b7f75dfc590..c2d4c8c18b5 100644 --- a/src/telegram/_videochat.py +++ b/src/telegram/_videochat.py @@ -25,11 +25,7 @@ from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import parse_sequence_arg, to_timedelta -from telegram._utils.datetime import ( - extract_tzinfo_from_defaults, - from_timestamp, - get_timedelta_value, -) +from telegram._utils.datetime import get_timedelta_value from telegram._utils.types import JSONDict, TimePeriod if TYPE_CHECKING: @@ -146,14 +142,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "VideoChatParticipantsInvited": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["users"] = User.de_list(data.get("users", []), bot) - return super().de_json(data=data, bot=bot) - class VideoChatScheduled(TelegramObject): """This object represents a service message about a video chat scheduled in the chat. @@ -193,15 +181,3 @@ def __init__( self._id_attrs = (self.start_date,) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "VideoChatScheduled": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["start_date"] = from_timestamp(data.get("start_date"), tzinfo=loc_tzinfo) - - return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_webhookinfo.py b/src/telegram/_webhookinfo.py index 94f6f7bf22e..9237f868371 100644 --- a/src/telegram/_webhookinfo.py +++ b/src/telegram/_webhookinfo.py @@ -24,7 +24,6 @@ from telegram._telegramobject import TelegramObject 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 if TYPE_CHECKING: @@ -162,18 +161,3 @@ def __init__( ) self._freeze() - - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "WebhookInfo": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - # 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"), tzinfo=loc_tzinfo - ) - - return super().de_json(data=data, bot=bot) diff --git a/tests/_files/test_sticker.py b/tests/_files/test_sticker.py index b3ee5178a0b..d76ce821e0f 100644 --- a/tests/_files/test_sticker.py +++ b/tests/_files/test_sticker.py @@ -69,11 +69,35 @@ class StickerTestBase: type = Sticker.REGULAR custom_emoji_id = "ThisIsSuchACustomEmojiID" needs_repainting = True + mask_position = MaskPosition(point="forehead", x_shift=0.1, y_shift=-0.1, scale=1.0) + thumbnail = PhotoSize( + file_id="1", + file_unique_id="1", + width=thumb_width, + height=thumb_height, + file_size=thumb_file_size, + ) sticker_file_id = "5a3128a4d2a04750b5b58397f3b5e812" sticker_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" premium_animation = File("this_is_an_id", "this_is_an_unique_id") + sticker = Sticker( + file_id=sticker_file_id, + file_unique_id=sticker_file_unique_id, + width=width, + height=height, + is_animated=is_animated, + is_video=is_video, + type=type, + emoji=emoji, + file_size=file_size, + mask_position=mask_position, + premium_animation=premium_animation, + custom_emoji_id=custom_emoji_id, + thumbnail=thumbnail, + needs_repainting=needs_repainting, + ) class TestStickerWithoutRequest(StickerTestBase): @@ -125,22 +149,8 @@ def test_to_dict(self, sticker): assert sticker_dict["type"] == sticker.type assert sticker_dict["needs_repainting"] == sticker.needs_repainting - def test_de_json(self, offline_bot, sticker): - json_dict = { - "file_id": self.sticker_file_id, - "file_unique_id": self.sticker_file_unique_id, - "width": self.width, - "height": self.height, - "is_animated": self.is_animated, - "is_video": self.is_video, - "thumbnail": sticker.thumbnail.to_dict(), - "emoji": self.emoji, - "file_size": self.file_size, - "premium_animation": self.premium_animation.to_dict(), - "type": self.type, - "custom_emoji_id": self.custom_emoji_id, - "needs_repainting": self.needs_repainting, - } + def test_de_json(self, offline_bot): + json_dict = self.sticker.to_dict() json_sticker = Sticker.de_json(json_dict, offline_bot) assert json_sticker.api_kwargs == {} @@ -152,7 +162,7 @@ def test_de_json(self, offline_bot, sticker): assert json_sticker.is_video == self.is_video assert json_sticker.emoji == self.emoji assert json_sticker.file_size == self.file_size - assert json_sticker.thumbnail == sticker.thumbnail + assert json_sticker.thumbnail == self.thumbnail assert json_sticker.premium_animation == self.premium_animation assert json_sticker.type == self.type assert json_sticker.custom_emoji_id == self.custom_emoji_id @@ -494,6 +504,13 @@ class StickerSetTestBase: sticker_type = Sticker.REGULAR contains_masks = True thumbnail = PhotoSize("thumb_file_id", "thumb_file_un_id", 100, 100, False) + sticker_set = StickerSet( + name, + title=title, + stickers=stickers, + sticker_type=sticker_type, + thumbnail=thumbnail, + ) class TestStickerSetWithoutRequest(StickerSetTestBase): @@ -503,22 +520,15 @@ def test_slot_behaviour(self): assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot, sticker): - name = f"test_by_{offline_bot.username}" - json_dict = { - "name": name, - "title": self.title, - "stickers": [x.to_dict() for x in self.stickers], - "thumbnail": sticker.thumbnail.to_dict(), - "sticker_type": self.sticker_type, - "contains_masks": self.contains_masks, - } + def test_de_json(self, offline_bot): + json_dict = self.sticker_set.to_dict() + json_dict["contains_masks"] = self.contains_masks sticker_set = StickerSet.de_json(json_dict, offline_bot) - assert sticker_set.name == name + assert sticker_set.name == self.name assert sticker_set.title == self.title assert sticker_set.stickers == tuple(self.stickers) - assert sticker_set.thumbnail == sticker.thumbnail + assert sticker_set.thumbnail == self.thumbnail assert sticker_set.sticker_type == self.sticker_type assert sticker_set.api_kwargs == {"contains_masks": self.contains_masks} From 4e0410aa14593c511044ea27bcd04e8f0581ff2f Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Mon, 30 Mar 2026 03:52:42 -0700 Subject: [PATCH 04/11] WIP: Fix tests All changes in this commit were done by Opus 4.6, some of which I'm not happy with, so that will be changed --- src/telegram/_botcommandscope.py | 48 +++------- src/telegram/_chatbackground.py | 62 +++++-------- src/telegram/_chatboost.py | 33 +++---- src/telegram/_chatmember.py | 50 +++------- src/telegram/_inline/inlinekeyboardmarkup.py | 16 ---- src/telegram/_menubutton.py | 40 ++------ src/telegram/_messageorigin.py | 44 +++------ src/telegram/_ownedgift.py | 45 ++------- src/telegram/_paidmedia.py | 40 ++------ .../_payment/stars/revenuewithdrawalstate.py | 37 ++------ .../_payment/stars/transactionpartner.py | 91 +++---------------- src/telegram/_reaction.py | 27 ++---- src/telegram/_telegramobject.py | 78 +++++++++++++++- 13 files changed, 209 insertions(+), 402 deletions(-) diff --git a/src/telegram/_botcommandscope.py b/src/telegram/_botcommandscope.py index cde05a79ac6..3a4b9077af2 100644 --- a/src/telegram/_botcommandscope.py +++ b/src/telegram/_botcommandscope.py @@ -19,7 +19,7 @@ # pylint: disable=redefined-builtin """This module contains objects representing Telegram bot command scopes.""" -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants from telegram._telegramobject import TelegramObject @@ -62,6 +62,19 @@ class BotCommandScope(TelegramObject): __slots__ = ("type",) + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "type", + { + "default": "BotCommandScopeDefault", + "all_private_chats": "BotCommandScopeAllPrivateChats", + "all_group_chats": "BotCommandScopeAllGroupChats", + "all_chat_administrators": "BotCommandScopeAllChatAdministrators", + "chat": "BotCommandScopeChat", + "chat_administrators": "BotCommandScopeChatAdministrators", + "chat_member": "BotCommandScopeChatMember", + }, + ) + DEFAULT: Final[str] = constants.BotCommandScopeType.DEFAULT """:const:`telegram.constants.BotCommandScopeType.DEFAULT`""" ALL_PRIVATE_CHATS: Final[str] = constants.BotCommandScopeType.ALL_PRIVATE_CHATS @@ -84,39 +97,6 @@ def __init__(self, type: str, *, api_kwargs: JSONDict | None = None): self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "BotCommandScope": - """Converts JSON data to the appropriate :class:`BotCommandScope` object, i.e. takes - care of selecting the correct subclass. - - Args: - data (dict[:obj:`str`, ...]): The JSON data. - bot (:class:`telegram.Bot`, optional): The bot associated with this object. Defaults to - :obj:`None`, in which case shortcut methods will not be available. - - .. versionchanged:: 21.4 - :paramref:`bot` is now optional and defaults to :obj:`None` - - Returns: - The Telegram object. - - """ - data = cls._parse_data(data) - - _class_mapping: dict[str, type[BotCommandScope]] = { - cls.DEFAULT: BotCommandScopeDefault, - cls.ALL_PRIVATE_CHATS: BotCommandScopeAllPrivateChats, - cls.ALL_GROUP_CHATS: BotCommandScopeAllGroupChats, - cls.ALL_CHAT_ADMINISTRATORS: BotCommandScopeAllChatAdministrators, - cls.CHAT: BotCommandScopeChat, - cls.CHAT_ADMINISTRATORS: BotCommandScopeChatAdministrators, - cls.CHAT_MEMBER: BotCommandScopeChatMember, - } - - if cls is BotCommandScope and data.get("type") in _class_mapping: - return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) - return super().de_json(data=data, bot=bot) - class BotCommandScopeDefault(BotCommandScope): """Represents the default scope of bot commands. Default commands are used if no commands with diff --git a/src/telegram/_chatbackground.py b/src/telegram/_chatbackground.py index f5bde6f05f7..8f4ed17547a 100644 --- a/src/telegram/_chatbackground.py +++ b/src/telegram/_chatbackground.py @@ -19,13 +19,13 @@ """This module contains objects related to chat backgrounds.""" from collections.abc import Sequence -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants from telegram._files.document import Document from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -57,6 +57,15 @@ class BackgroundFill(TelegramObject): __slots__ = ("type",) + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "type", + { + "solid": "BackgroundFillSolid", + "gradient": "BackgroundFillGradient", + "freeform_gradient": "BackgroundFillFreeformGradient", + }, + ) + SOLID: Final[constants.BackgroundFillType] = constants.BackgroundFillType.SOLID """:const:`telegram.constants.BackgroundFillType.SOLID`""" GRADIENT: Final[constants.BackgroundFillType] = constants.BackgroundFillType.GRADIENT @@ -79,22 +88,6 @@ def __init__( self._id_attrs = (self.type,) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "BackgroundFill": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - _class_mapping: dict[str, type[BackgroundFill]] = { - cls.SOLID: BackgroundFillSolid, - cls.GRADIENT: BackgroundFillGradient, - cls.FREEFORM_GRADIENT: BackgroundFillFreeformGradient, - } - - if cls is BackgroundFill and data.get("type") in _class_mapping: - return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) - - return super().de_json(data=data, bot=bot) - class BackgroundFillSolid(BackgroundFill): """ @@ -243,6 +236,16 @@ class BackgroundType(TelegramObject): __slots__ = ("type",) + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "type", + { + "fill": "BackgroundTypeFill", + "wallpaper": "BackgroundTypeWallpaper", + "pattern": "BackgroundTypePattern", + "chat_theme": "BackgroundTypeChatTheme", + }, + ) + FILL: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.FILL """:const:`telegram.constants.BackgroundTypeType.FILL`""" WALLPAPER: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.WALLPAPER @@ -265,29 +268,6 @@ def __init__( self._id_attrs = (self.type,) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "BackgroundType": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - _class_mapping: dict[str, type[BackgroundType]] = { - cls.FILL: BackgroundTypeFill, - cls.WALLPAPER: BackgroundTypeWallpaper, - cls.PATTERN: BackgroundTypePattern, - cls.CHAT_THEME: BackgroundTypeChatTheme, - } - - if cls is BackgroundType and data.get("type") in _class_mapping: - return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) - - if "fill" in data: - data["fill"] = de_json_optional(data.get("fill"), BackgroundFill, bot) - - if "document" in data: - data["document"] = de_json_optional(data.get("document"), Document, bot) - - return super().de_json(data=data, bot=bot) - class BackgroundTypeFill(BackgroundType): """ diff --git a/src/telegram/_chatboost.py b/src/telegram/_chatboost.py index 4bc184900aa..c1be871bc8f 100644 --- a/src/telegram/_chatboost.py +++ b/src/telegram/_chatboost.py @@ -20,15 +20,14 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants from telegram._chat import Chat from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -94,6 +93,15 @@ class ChatBoostSource(TelegramObject): __slots__ = ("source",) + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "source", + { + "premium": "ChatBoostSourcePremium", + "gift_code": "ChatBoostSourceGiftCode", + "giveaway": "ChatBoostSourceGiveaway", + }, + ) + PREMIUM: Final[str] = constants.ChatBoostSources.PREMIUM """:const:`telegram.constants.ChatBoostSources.PREMIUM`""" GIFT_CODE: Final[str] = constants.ChatBoostSources.GIFT_CODE @@ -110,25 +118,6 @@ def __init__(self, source: str, *, api_kwargs: JSONDict | None = None): self._id_attrs = (self.source,) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatBoostSource": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - _class_mapping: dict[str, type[ChatBoostSource]] = { - cls.PREMIUM: ChatBoostSourcePremium, - cls.GIFT_CODE: ChatBoostSourceGiftCode, - cls.GIVEAWAY: ChatBoostSourceGiveaway, - } - - if cls is ChatBoostSource and data.get("source") in _class_mapping: - return _class_mapping[data.pop("source")].de_json(data=data, bot=bot) - - if "user" in data: - data["user"] = de_json_optional(data.get("user"), User, bot) - - return super().de_json(data=data, bot=bot) - class ChatBoostSourcePremium(ChatBoostSource): """ diff --git a/src/telegram/_chatmember.py b/src/telegram/_chatmember.py index be55c8469e8..2ee76154444 100644 --- a/src/telegram/_chatmember.py +++ b/src/telegram/_chatmember.py @@ -19,14 +19,12 @@ """This module contains an object that represents a Telegram ChatMember.""" import datetime as dtm -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -77,6 +75,18 @@ class ChatMember(TelegramObject): __slots__ = ("status", "user") + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "status", + { + "creator": "ChatMemberOwner", + "administrator": "ChatMemberAdministrator", + "member": "ChatMemberMember", + "restricted": "ChatMemberRestricted", + "left": "ChatMemberLeft", + "kicked": "ChatMemberBanned", + }, + ) + ADMINISTRATOR: Final[str] = constants.ChatMemberStatus.ADMINISTRATOR """:const:`telegram.constants.ChatMemberStatus.ADMINISTRATOR`""" OWNER: Final[str] = constants.ChatMemberStatus.OWNER @@ -106,38 +116,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatMember": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - _class_mapping: dict[str, type[ChatMember]] = { - cls.OWNER: ChatMemberOwner, - cls.ADMINISTRATOR: ChatMemberAdministrator, - cls.MEMBER: ChatMemberMember, - cls.RESTRICTED: ChatMemberRestricted, - cls.LEFT: ChatMemberLeft, - cls.BANNED: ChatMemberBanned, - } - - if cls is ChatMember and data.get("status") in _class_mapping: - return _class_mapping[data.pop("status")].de_json(data=data, bot=bot) - - data["user"] = de_json_optional(data.get("user"), User, bot) - if "until_date" in data: - # Get the local timezone from the bot if it has defaults - loc_tzinfo = extract_tzinfo_from_defaults(bot) - - data["until_date"] = from_timestamp(data.get("until_date"), tzinfo=loc_tzinfo) - - # This is a deprecated field that TG still returns for backwards compatibility - # Let's filter it out to speed up the de-json process - if cls is ChatMemberRestricted and data.get("can_send_media_messages") is not None: - api_kwargs = {"can_send_media_messages": data.pop("can_send_media_messages")} - return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) - - return super().de_json(data=data, bot=bot) - class ChatMemberOwner(ChatMember): """ @@ -600,6 +578,8 @@ class ChatMemberRestricted(ChatMember): "until_date", ) + __REMOVED_API_FIELDS__: ClassVar[frozenset[str]] = frozenset({"can_send_media_messages"}) + def __init__( self, user: User, diff --git a/src/telegram/_inline/inlinekeyboardmarkup.py b/src/telegram/_inline/inlinekeyboardmarkup.py index 166bdbd64bc..540a84ce6e5 100644 --- a/src/telegram/_inline/inlinekeyboardmarkup.py +++ b/src/telegram/_inline/inlinekeyboardmarkup.py @@ -91,22 +91,6 @@ def __init__( self._freeze() - # TODO: Come back to this and modify TGObject to parse nested Sequence's - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "InlineKeyboardMarkup": - """See :meth:`telegram.TelegramObject.de_json`.""" - - keyboard = [] - for row in data["inline_keyboard"]: - tmp = [] - for col in row: - btn = InlineKeyboardButton.de_json(col, bot) - if btn: - tmp.append(btn) - keyboard.append(tmp) - - return cls(keyboard) - @classmethod def from_button(cls, button: InlineKeyboardButton, **kwargs: object) -> "InlineKeyboardMarkup": """Shortcut for:: diff --git a/src/telegram/_menubutton.py b/src/telegram/_menubutton.py index 3788f453949..92a8ecf88c5 100644 --- a/src/telegram/_menubutton.py +++ b/src/telegram/_menubutton.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains objects related to Telegram menu buttons.""" -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants from telegram._telegramobject import TelegramObject @@ -56,6 +56,15 @@ class MenuButton(TelegramObject): __slots__ = ("type",) + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "type", + { + "commands": "MenuButtonCommands", + "web_app": "MenuButtonWebApp", + "default": "MenuButtonDefault", + }, + ) + def __init__( self, type: str, @@ -69,35 +78,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "MenuButton": - """Converts JSON data to the appropriate :class:`MenuButton` object, i.e. takes - care of selecting the correct subclass. - - Args: - data (dict[:obj:`str`, ...]): The JSON data. - bot (:class:`telegram.Bot`, optional): The bot associated with this object. Defaults to - :obj:`None`, in which case shortcut methods will not be available. - - .. versionchanged:: 21.4 - :paramref:`bot` is now optional and defaults to :obj:`None` - - Returns: - The Telegram object. - - """ - data = cls._parse_data(data) - - _class_mapping: dict[str, type[MenuButton]] = { - cls.COMMANDS: MenuButtonCommands, - cls.WEB_APP: MenuButtonWebApp, - cls.DEFAULT: MenuButtonDefault, - } - - if cls is MenuButton and data.get("type") in _class_mapping: - return _class_mapping[data.pop("type")].de_json(data, bot=bot) - return super().de_json(data=data, bot=bot) - COMMANDS: Final[str] = constants.MenuButtonType.COMMANDS """:const:`telegram.constants.MenuButtonType.COMMANDS`""" WEB_APP: Final[str] = constants.MenuButtonType.WEB_APP diff --git a/src/telegram/_messageorigin.py b/src/telegram/_messageorigin.py index 767f5a4a0af..3010e0aefab 100644 --- a/src/telegram/_messageorigin.py +++ b/src/telegram/_messageorigin.py @@ -19,15 +19,13 @@ """This module contains the classes that represent Telegram MessageOigin.""" import datetime as dtm -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants from telegram._chat import Chat from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -68,6 +66,16 @@ class MessageOrigin(TelegramObject): "type", ) + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "type", + { + "user": "MessageOriginUser", + "hidden_user": "MessageOriginHiddenUser", + "chat": "MessageOriginChat", + "channel": "MessageOriginChannel", + }, + ) + USER: Final[str] = constants.MessageOriginType.USER """:const:`telegram.constants.MessageOriginType.USER`""" HIDDEN_USER: Final[str] = constants.MessageOriginType.HIDDEN_USER @@ -95,36 +103,6 @@ def __init__( ) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "MessageOrigin": - """Converts JSON data to the appropriate :class:`MessageOrigin` object, i.e. takes - care of selecting the correct subclass. - """ - data = cls._parse_data(data) - - _class_mapping: dict[str, type[MessageOrigin]] = { - cls.USER: MessageOriginUser, - cls.HIDDEN_USER: MessageOriginHiddenUser, - cls.CHAT: MessageOriginChat, - cls.CHANNEL: MessageOriginChannel, - } - if cls is MessageOrigin and data.get("type") in _class_mapping: - return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) - - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo) - - if "sender_user" in data: - data["sender_user"] = de_json_optional(data.get("sender_user"), User, bot) - - if "sender_chat" in data: - data["sender_chat"] = de_json_optional(data.get("sender_chat"), Chat, bot) - - if "chat" in data: - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - - return super().de_json(data=data, bot=bot) - class MessageOriginUser(MessageOrigin): """ diff --git a/src/telegram/_ownedgift.py b/src/telegram/_ownedgift.py index b142a26cf8a..685ec96472a 100644 --- a/src/telegram/_ownedgift.py +++ b/src/telegram/_ownedgift.py @@ -20,7 +20,7 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants from telegram._gifts import Gift @@ -29,7 +29,7 @@ from telegram._uniquegift import UniqueGift from telegram._user import User from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.entities import parse_message_entities, parse_message_entity from telegram._utils.types import JSONDict @@ -63,6 +63,14 @@ class OwnedGift(TelegramObject): UNIQUE: Final[str] = constants.OwnedGiftType.UNIQUE """:const:`telegram.constants.OwnedGiftType.UNIQUE`""" + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "type", + { + "regular": "OwnedGiftRegular", + "unique": "OwnedGiftUnique", + }, + ) + def __init__( self, type: str, # pylint: disable=redefined-builtin @@ -75,31 +83,6 @@ def __init__( self._id_attrs = (self.type,) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "OwnedGift": - """Converts JSON data to the appropriate :class:`OwnedGift` object, i.e. takes - care of selecting the correct subclass. - - Args: - data (dict[:obj:`str`, ...]): The JSON data. - bot (:class:`telegram.Bot`, optional): The bot associated with this object. - - Returns: - The Telegram object. - - """ - data = cls._parse_data(data) - - _class_mapping: dict[str, type[OwnedGift]] = { - cls.REGULAR: OwnedGiftRegular, - cls.UNIQUE: OwnedGiftUnique, - } - - if cls is OwnedGift and data.get("type") in _class_mapping: - return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) - - return super().de_json(data=data, bot=bot) - class OwnedGifts(TelegramObject): """Contains the list of gifts received and owned by a user or a chat. @@ -145,14 +128,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "OwnedGifts": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - data["gifts"] = de_list_optional(data.get("gifts"), OwnedGift, bot) - return super().de_json(data=data, bot=bot) - class OwnedGiftRegular(OwnedGift): """Describes a regular gift owned by a user or a chat. diff --git a/src/telegram/_paidmedia.py b/src/telegram/_paidmedia.py index 2353f4910d2..97553fb4ef7 100644 --- a/src/telegram/_paidmedia.py +++ b/src/telegram/_paidmedia.py @@ -20,7 +20,7 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants from telegram._files.photosize import PhotoSize @@ -60,6 +60,15 @@ class PaidMedia(TelegramObject): __slots__ = ("type",) + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "type", + { + "preview": "PaidMediaPreview", + "photo": "PaidMediaPhoto", + "video": "PaidMediaVideo", + }, + ) + PREVIEW: Final[str] = constants.PaidMediaType.PREVIEW """:const:`telegram.constants.PaidMediaType.PREVIEW`""" PHOTO: Final[str] = constants.PaidMediaType.PHOTO @@ -79,35 +88,6 @@ def __init__( self._id_attrs = (self.type,) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PaidMedia": - """Converts JSON data to the appropriate :class:`PaidMedia` object, i.e. takes - care of selecting the correct subclass. - - Args: - data (dict[:obj:`str`, ...]): The JSON data. - bot (:class:`telegram.Bot`, optional): The bot associated with this object. - - Returns: - The Telegram object. - - """ - data = cls._parse_data(data) - - _class_mapping: dict[str, type[PaidMedia]] = { - cls.PREVIEW: PaidMediaPreview, - cls.PHOTO: PaidMediaPhoto, - cls.VIDEO: PaidMediaVideo, - } - - if cls is PaidMedia and data.get("type") in _class_mapping: - return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) - - if "duration" in data: - data["duration"] = dtm.timedelta(seconds=s) if (s := data.get("duration")) else None - - return super().de_json(data=data, bot=bot) - class PaidMediaPreview(PaidMedia): """The paid media isn't available before the payment. diff --git a/src/telegram/_payment/stars/revenuewithdrawalstate.py b/src/telegram/_payment/stars/revenuewithdrawalstate.py index 2fcea224a01..83199b85579 100644 --- a/src/telegram/_payment/stars/revenuewithdrawalstate.py +++ b/src/telegram/_payment/stars/revenuewithdrawalstate.py @@ -20,7 +20,7 @@ """This module contains the classes for Telegram Stars Revenue Withdrawals.""" import datetime as dtm -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants from telegram._telegramobject import TelegramObject @@ -53,6 +53,15 @@ class RevenueWithdrawalState(TelegramObject): __slots__ = ("type",) + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "type", + { + "pending": "RevenueWithdrawalStatePending", + "succeeded": "RevenueWithdrawalStateSucceeded", + "failed": "RevenueWithdrawalStateFailed", + }, + ) + PENDING: Final[str] = constants.RevenueWithdrawalStateType.PENDING """:const:`telegram.constants.RevenueWithdrawalStateType.PENDING`""" SUCCEEDED: Final[str] = constants.RevenueWithdrawalStateType.SUCCEEDED @@ -67,32 +76,6 @@ def __init__(self, type: str, *, api_kwargs: JSONDict | None = None) -> None: self._id_attrs = (self.type,) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "RevenueWithdrawalState": - """Converts JSON data to the appropriate :class:`RevenueWithdrawalState` object, i.e. takes - care of selecting the correct subclass. - - Args: - data (dict[:obj:`str`, ...]): The JSON data. - bot (:class:`telegram.Bot`): The bot associated with this object. - - Returns: - The Telegram object. - - """ - data = cls._parse_data(data) - - _class_mapping: dict[str, type[RevenueWithdrawalState]] = { - cls.PENDING: RevenueWithdrawalStatePending, - cls.SUCCEEDED: RevenueWithdrawalStateSucceeded, - cls.FAILED: RevenueWithdrawalStateFailed, - } - - if cls is RevenueWithdrawalState and data.get("type") in _class_mapping: - return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) - - return super().de_json(data=data, bot=bot) - class RevenueWithdrawalStatePending(RevenueWithdrawalState): """The withdrawal is in progress. diff --git a/src/telegram/_payment/stars/transactionpartner.py b/src/telegram/_payment/stars/transactionpartner.py index 407447d2f9a..ab0142c4da1 100644 --- a/src/telegram/_payment/stars/transactionpartner.py +++ b/src/telegram/_payment/stars/transactionpartner.py @@ -20,7 +20,7 @@ """This module contains the classes for Telegram Stars transaction partners.""" from collections.abc import Sequence -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants from telegram._chat import Chat @@ -32,8 +32,6 @@ from telegram._utils.argumentparsing import ( parse_sequence_arg, to_timedelta, - de_json_optional, - de_list_optional, ) from telegram._utils.types import JSONDict, TimePeriod @@ -75,6 +73,19 @@ class TransactionPartner(TelegramObject): __slots__ = ("type",) + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "type", + { + "affiliate_program": "TransactionPartnerAffiliateProgram", + "chat": "TransactionPartnerChat", + "fragment": "TransactionPartnerFragment", + "other": "TransactionPartnerOther", + "telegram_ads": "TransactionPartnerTelegramAds", + "telegram_api": "TransactionPartnerTelegramApi", + "user": "TransactionPartnerUser", + }, + ) + AFFILIATE_PROGRAM: Final[str] = constants.TransactionPartnerType.AFFILIATE_PROGRAM """:const:`telegram.constants.TransactionPartnerType.AFFILIATE_PROGRAM` @@ -103,36 +114,6 @@ def __init__(self, type: str, *, api_kwargs: JSONDict | None = None) -> None: self._id_attrs = (self.type,) self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "TransactionPartner": - """Converts JSON data to the appropriate :class:`TransactionPartner` object, i.e. takes - care of selecting the correct subclass. - - Args: - data (dict[:obj:`str`, ...]): The JSON data. - bot (:class:`telegram.Bot`): The bot associated with this object. - - Returns: - The Telegram object. - - """ - data = cls._parse_data(data) - - _class_mapping: dict[str, type[TransactionPartner]] = { - cls.AFFILIATE_PROGRAM: TransactionPartnerAffiliateProgram, - cls.CHAT: TransactionPartnerChat, - cls.FRAGMENT: TransactionPartnerFragment, - cls.USER: TransactionPartnerUser, - cls.TELEGRAM_ADS: TransactionPartnerTelegramAds, - cls.TELEGRAM_API: TransactionPartnerTelegramApi, - cls.OTHER: TransactionPartnerOther, - } - - if cls is TransactionPartner and data.get("type") in _class_mapping: - return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) - - return super().de_json(data=data, bot=bot) - class TransactionPartnerAffiliateProgram(TransactionPartner): """Describes the affiliate program that issued the affiliate commission received via this @@ -177,17 +158,6 @@ def __init__( self.commission_per_mille, ) - @classmethod - def de_json( - cls, data: JSONDict, bot: "Bot | None" = None - ) -> "TransactionPartnerAffiliateProgram": - """See :meth:`telegram.TransactionPartner.de_json`.""" - data = cls._parse_data(data) - - data["sponsor_user"] = de_json_optional(data.get("sponsor_user"), User, bot) - - return super().de_json(data=data, bot=bot) # type: ignore[return-value] - class TransactionPartnerChat(TransactionPartner): """Describes a transaction with a chat. @@ -232,16 +202,6 @@ def __init__( self.chat, ) - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "TransactionPartnerChat": - """See :meth:`telegram.TransactionPartner.de_json`.""" - data = cls._parse_data(data) - - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - data["gift"] = de_json_optional(data.get("gift"), Gift, bot) - - return super().de_json(data=data, bot=bot) # type: ignore[return-value] - class TransactionPartnerFragment(TransactionPartner): """Describes a withdrawal transaction with Fragment. @@ -272,17 +232,6 @@ def __init__( with self._unfrozen(): self.withdrawal_state: RevenueWithdrawalState | None = withdrawal_state - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "TransactionPartnerFragment": - """See :meth:`telegram.TransactionPartner.de_json`.""" - data = cls._parse_data(data) - - data["withdrawal_state"] = de_json_optional( - data.get("withdrawal_state"), RevenueWithdrawalState, bot - ) - - return super().de_json(data=data, bot=bot) # type: ignore[return-value] - class TransactionPartnerUser(TransactionPartner): """Describes a transaction with a user. @@ -448,18 +397,6 @@ def __init__( self.transaction_type, ) - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "TransactionPartnerUser": - """See :meth:`telegram.TransactionPartner.de_json`.""" - data = cls._parse_data(data) - - data["user"] = de_json_optional(data.get("user"), User, bot) - data["affiliate"] = de_json_optional(data.get("affiliate"), AffiliateInfo, bot) - data["paid_media"] = de_list_optional(data.get("paid_media"), PaidMedia, bot) - data["gift"] = de_json_optional(data.get("gift"), Gift, bot) - - return super().de_json(data=data, bot=bot) # type: ignore[return-value] - class TransactionPartnerOther(TransactionPartner): """Describes a transaction with an unknown partner. diff --git a/src/telegram/_reaction.py b/src/telegram/_reaction.py index 69670e5dceb..adf8b219cf0 100644 --- a/src/telegram/_reaction.py +++ b/src/telegram/_reaction.py @@ -19,7 +19,7 @@ # pylint: disable=redefined-builtin """This module contains objects that represents a Telegram ReactionType.""" -from typing import TYPE_CHECKING, Final, Literal +from typing import TYPE_CHECKING, ClassVar, Final, Literal from telegram import constants from telegram._telegramobject import TelegramObject @@ -53,6 +53,15 @@ class ReactionType(TelegramObject): __slots__ = ("type",) + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( + "type", + { + "emoji": "ReactionTypeEmoji", + "custom_emoji": "ReactionTypeCustomEmoji", + "paid": "ReactionTypePaid", + }, + ) + EMOJI: Final[constants.ReactionType] = constants.ReactionType.EMOJI """:const:`telegram.constants.ReactionType.EMOJI`""" CUSTOM_EMOJI: Final[constants.ReactionType] = constants.ReactionType.CUSTOM_EMOJI @@ -75,22 +84,6 @@ def __init__( self._freeze() - @classmethod - def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ReactionType": - """See :meth:`telegram.TelegramObject.de_json`.""" - data = cls._parse_data(data) - - _class_mapping: dict[str, type[ReactionType]] = { - cls.EMOJI: ReactionTypeEmoji, - cls.CUSTOM_EMOJI: ReactionTypeCustomEmoji, - cls.PAID: ReactionTypePaid, - } - - if cls is ReactionType and data.get("type") in _class_mapping: - return _class_mapping[data.pop("type")].de_json(data, bot) - - return super().de_json(data=data, bot=bot) - class ReactionTypeEmoji(ReactionType): """ diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index 791da3c048d..29747408800 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -115,11 +115,16 @@ class TelegramObject: # be intercepted by de_json and forwarded into api_kwargs before construction. __REMOVED_API_FIELDS__: ClassVar[frozenset[str]] = frozenset() - # def __init_subclass__(cls, **kwargs) -> None: - # super().__init_subclass__(**kwargs) + # Subclasses may define a "sequence default" set: parameter names that accept + # Sequence[...] (or Sequence[Sequence[...]]) arguments and should default to + # an empty tuple when the key is absent from incoming JSON data. + __DE_JSON_LIST_FIELDS__: ClassVar[frozenset[str]] = frozenset() - # print("Building de_json plan for", cls.__name__) - # cls.__DE_JSON_PLAN__ = cls._build_plan() + # Delegator base classes may define a dispatch mapping to route de_json to + # the correct subclass. Format: (dispatch_key, {value: "ClassName", ...}). + # The dispatch_key is the JSON field name (e.g. "type", "source", "status"). + # The values are class name strings, resolved lazily via the telegram namespace. + __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = None def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: # Setting _frozen to `False` here means that classes without arguments still need to @@ -432,6 +437,7 @@ def _build_plan(cls) -> dict[str, Any]: * ``datetime`` fields → :func:`~telegram._utils.datetime.from_timestamp` * ``TelegramObject`` fields → ``TargetCls.de_json(...)`` * ``Sequence[TelegramObject]`` fields → ``TargetCls.de_list(...)`` + * ``Sequence[Sequence[TelegramObject]]`` fields → nested de_list """ init_fn = cls.__dict__.get("__init__") if init_fn is None: @@ -441,9 +447,11 @@ def _build_plan(cls) -> dict[str, Any]: if "__DE_JSON_PLAN__" not in parent.__dict__: parent._build_plan() cls.__DE_JSON_PLAN__ = parent.__DE_JSON_PLAN__ + cls.__DE_JSON_LIST_FIELDS__ = parent.__DE_JSON_LIST_FIELDS__ return cls.__DE_JSON_PLAN__ plan: dict[str, Any] = {} + list_fields: set[str] = set() globalns: dict[str, object] = getattr(init_fn, "__globals__", {}) tg_ns = _telegram_ns() @@ -469,7 +477,7 @@ def _build_plan(cls) -> dict[str, Any]: ) elif isinstance(inner, type) and issubclass(inner, TelegramObject): print("Adding de_json plan for", cls.__name__, name, "→", inner.__name__) - plan[name] = lambda v, b, _c=inner: _c.de_json(v, b) if isinstance(v, dict) else v + plan[name] = lambda v, b, _c=inner: v if isinstance(v, _c) else _c.de_json(v, b) elif origin is Sequence: args = get_args(inner) if not args: @@ -480,15 +488,44 @@ def _build_plan(cls) -> dict[str, Any]: print(f"Resolving forward reference for {cls.__name__}.{name}: {item_type}") # with contextlib.suppress(Exception): item_type = eval(item_type, globalns, tg_ns) # noqa: S307 + + # Check for nested sequences: Sequence[Sequence[TelegramObject]] + item_origin = get_origin(item_type) + if item_origin is Sequence: + inner_args = get_args(item_type) + if inner_args: + inner_item: object = inner_args[0] + if isinstance(inner_item, str): + print( + "Resolving nested forward ref for" + f" {cls.__name__}.{name}: {inner_item}" + ) + inner_item = eval(inner_item, globalns, tg_ns) # noqa: S307 + if isinstance(inner_item, type) and issubclass(inner_item, TelegramObject): + print( + "Adding nested de_list plan for", + cls.__name__, + name, + "→", + inner_item.__name__, + ) + plan[name] = lambda v, b, _c=inner_item: ( + [_c.de_list(row, b) for row in v] if isinstance(v, list) else v + ) + list_fields.add(name) + continue + if isinstance(item_type, type) and issubclass(item_type, TelegramObject): print("Adding de_list plan for", cls.__name__, name, "→", item_type.__name__) plan[name] = lambda v, b, _c=item_type: ( _c.de_list(v, b) if isinstance(v, list) else v ) + list_fields.add(name) else: print(f"No de_json plan for {cls.__name__}.{name} (annotation: {ann})") cls.__DE_JSON_PLAN__ = plan + cls.__DE_JSON_LIST_FIELDS__ = frozenset(list_fields) return plan @classmethod @@ -538,6 +575,21 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel """ data = cls._parse_data(data) + # Dispatch to subclass for delegator classes (e.g. TransactionPartner, ChatMember). + # Only dispatches when __DE_JSON_DISPATCH__ is defined directly on cls (not inherited). + if "__DE_JSON_DISPATCH__" in cls.__dict__: + dispatch_key, dispatch_mapping = cls.__DE_JSON_DISPATCH__ + target_name = dispatch_mapping.get(data.get(dispatch_key)) + if target_name is not None: + # Resolve string class name → actual class (lazy, cached) + if isinstance(target_name, str): + target_cls = _telegram_ns()[target_name] + dispatch_mapping[data[dispatch_key]] = target_cls + else: + target_cls = target_name + data.pop(dispatch_key) + return target_cls.de_json(data=data, bot=bot) + # Move removed/legacy API fields into api_kwargs api_kwargs: JSONDict | None = None if cls.__REMOVED_API_FIELDS__: @@ -560,6 +612,22 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel elif "from_user" not in data: data["from_user"] = None + # Default missing list fields to empty tuple so that required Sequence + # parameters don't cause TypeErrors during construction. + if cls.__DE_JSON_LIST_FIELDS__: + for key in cls.__DE_JSON_LIST_FIELDS__: + if key not in data: + data[key] = () + + # Default missing plan keys to None (mimicking old de_json_optional behavior). + # This ensures required TelegramObject/datetime fields get None when missing + # from the data, which is what the old manual de_json implementations did. + # List fields are already handled above with empty tuples. + if plan: + for key in plan: + if key not in data: + data[key] = None + # Only for classes with a plan. We avoid looping through the data keys for no plan classes. if plan: # This is O(M). This part is unavoidable. From 50d9d63c11c096e47d13aec5df7eee880e644f63 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Mon, 30 Mar 2026 05:11:25 -0700 Subject: [PATCH 05/11] WIP: Remove extraneous cls field --- src/telegram/_telegramobject.py | 147 +++++++++++++------------------- 1 file changed, 61 insertions(+), 86 deletions(-) diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index 29747408800..b9a4e7f4bb5 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -57,6 +57,43 @@ def _unwrap_optional(ann: object) -> object: return ann +def _make_seq_transform( + item_type: object, + globalns: dict[str, object], + tg_ns: dict[str, object], + cls_name: str, + field_name: str, +) -> Any | None: + """Recursively build a ``(value, bot) → transformed_value`` lambda for a Sequence item type. + + Returns :obj:`None` if the item type does not require any transformation. + """ + if isinstance(item_type, str): + print(f"Resolving forward reference for {cls_name}.{field_name}: {item_type}") + item_type = eval(item_type, globalns, tg_ns) # noqa: S307 + + item_origin = get_origin(item_type) + if item_origin is Sequence: + inner_args = get_args(item_type) + if not inner_args: + return None + inner_fn = _make_seq_transform(inner_args[0], globalns, tg_ns, cls_name, field_name) + if inner_fn is None: + return None + print(f"Adding nested sequence plan for {cls_name}.{field_name}") + return lambda v, b, _f=inner_fn: ( + () if v is None else [_f(row, b) for row in v] if isinstance(v, list) else v + ) + + if isinstance(item_type, type) and issubclass(item_type, TelegramObject): + print(f"Adding de_list plan for {cls_name}.{field_name} → {item_type.__name__}") + return lambda v, b, _c=item_type: ( + () if v is None else _c.de_list(v, b) if isinstance(v, list) else v + ) + + return None + + class TelegramObject: """Base class for most Telegram objects. @@ -104,10 +141,9 @@ class TelegramObject: # unless it's overridden __INIT_PARAMS_CHECK: type["TelegramObject"] | None = None - # Per-class declarative de_json plan (built lazily on first de_json call). + # Per-class declarative de_json plan. # Maps parameter name → transform callable. Each subclass gets its own - # plan via _build_plan(); the ``cls.__dict__`` check in de_json ensures - # inherited plans are never confused with the class's own plan. + # plan via _build_plan() __DE_JSON_PLAN__: ClassVar[dict[str, Any]] = {} # Subclasses may declare field names that Telegram still returns for backwards @@ -115,15 +151,11 @@ class TelegramObject: # be intercepted by de_json and forwarded into api_kwargs before construction. __REMOVED_API_FIELDS__: ClassVar[frozenset[str]] = frozenset() - # Subclasses may define a "sequence default" set: parameter names that accept - # Sequence[...] (or Sequence[Sequence[...]]) arguments and should default to - # an empty tuple when the key is absent from incoming JSON data. - __DE_JSON_LIST_FIELDS__: ClassVar[frozenset[str]] = frozenset() - - # Delegator base classes may define a dispatch mapping to route de_json to - # the correct subclass. Format: (dispatch_key, {value: "ClassName", ...}). + # Delegator base classes (e.g. TransactionPartner) may define a dispatch mapping to route + # de_json to the correct subclass. + # Format: (dispatch_key, {value: "ClassName", ...}). # The dispatch_key is the JSON field name (e.g. "type", "source", "status"). - # The values are class name strings, resolved lazily via the telegram namespace. + # The values are class name strings, e.g. "TransactionPartnerChat". __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = None def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: @@ -447,11 +479,9 @@ def _build_plan(cls) -> dict[str, Any]: if "__DE_JSON_PLAN__" not in parent.__dict__: parent._build_plan() cls.__DE_JSON_PLAN__ = parent.__DE_JSON_PLAN__ - cls.__DE_JSON_LIST_FIELDS__ = parent.__DE_JSON_LIST_FIELDS__ return cls.__DE_JSON_PLAN__ plan: dict[str, Any] = {} - list_fields: set[str] = set() globalns: dict[str, object] = getattr(init_fn, "__globals__", {}) tg_ns = _telegram_ns() @@ -471,61 +501,28 @@ def _build_plan(cls) -> dict[str, Any]: if inner is dtm.datetime: plan[name] = lambda value, bot: ( - value + None + if value is None + else value if isinstance(value, dtm.datetime) else from_timestamp(value, tzinfo=extract_tzinfo_from_defaults(bot)) ) elif isinstance(inner, type) and issubclass(inner, TelegramObject): print("Adding de_json plan for", cls.__name__, name, "→", inner.__name__) - plan[name] = lambda v, b, _c=inner: v if isinstance(v, _c) else _c.de_json(v, b) + plan[name] = lambda v, b, _c=inner: ( + None if v is None else v if isinstance(v, _c) else _c.de_json(v, b) + ) elif origin is Sequence: args = get_args(inner) if not args: continue - item_type: object = args[0] - # inspect.signature doesn't resolve the forward ref inside Sequence for some reason - if isinstance(item_type, str): - print(f"Resolving forward reference for {cls.__name__}.{name}: {item_type}") - # with contextlib.suppress(Exception): - item_type = eval(item_type, globalns, tg_ns) # noqa: S307 - - # Check for nested sequences: Sequence[Sequence[TelegramObject]] - item_origin = get_origin(item_type) - if item_origin is Sequence: - inner_args = get_args(item_type) - if inner_args: - inner_item: object = inner_args[0] - if isinstance(inner_item, str): - print( - "Resolving nested forward ref for" - f" {cls.__name__}.{name}: {inner_item}" - ) - inner_item = eval(inner_item, globalns, tg_ns) # noqa: S307 - if isinstance(inner_item, type) and issubclass(inner_item, TelegramObject): - print( - "Adding nested de_list plan for", - cls.__name__, - name, - "→", - inner_item.__name__, - ) - plan[name] = lambda v, b, _c=inner_item: ( - [_c.de_list(row, b) for row in v] if isinstance(v, list) else v - ) - list_fields.add(name) - continue - - if isinstance(item_type, type) and issubclass(item_type, TelegramObject): - print("Adding de_list plan for", cls.__name__, name, "→", item_type.__name__) - plan[name] = lambda v, b, _c=item_type: ( - _c.de_list(v, b) if isinstance(v, list) else v - ) - list_fields.add(name) + fn = _make_seq_transform(args[0], globalns, tg_ns, cls.__name__, name) + if fn is not None: + plan[name] = fn else: print(f"No de_json plan for {cls.__name__}.{name} (annotation: {ann})") cls.__DE_JSON_PLAN__ = plan - cls.__DE_JSON_LIST_FIELDS__ = frozenset(list_fields) return plan @classmethod @@ -604,40 +601,18 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel cls._build_plan() plan = cls.__DE_JSON_PLAN__ - # Rename "from" → "from_user" before the transform loop so we can - # iterate data keys without adding/removing entries inside the loop. - if "from_user" in plan: # membership checks are O(1) - if "from" in data: - data["from_user"] = data.pop("from") - elif "from_user" not in data: - data["from_user"] = None - - # Default missing list fields to empty tuple so that required Sequence - # parameters don't cause TypeErrors during construction. - if cls.__DE_JSON_LIST_FIELDS__: - for key in cls.__DE_JSON_LIST_FIELDS__: - if key not in data: - data[key] = () - - # Default missing plan keys to None (mimicking old de_json_optional behavior). - # This ensures required TelegramObject/datetime fields get None when missing - # from the data, which is what the old manual de_json implementations did. - # List fields are already handled above with empty tuples. - if plan: - for key in plan: - if key not in data: - data[key] = None + # Rename "from" → "from_user" when the plan expects it and "from" is present in data. + if "from_user" in plan and "from" in data: + data["from_user"] = data.pop("from") - # Only for classes with a plan. We avoid looping through the data keys for no plan classes. + # Apply plan transforms for all plan keys. Lambdas handle None (missing/null) themselves: + # TelegramObject → None, datetime → None, Sequence[TelegramObject] → (). + # Iterating plan (not data) is O(plan size) and also handles missing keys correctly. if plan: - # This is O(M). This part is unavoidable. print(f"Data length: {len(data)}, plan length: {len(plan)}") - for key in data: - if key in plan and data[key] is not None: - print("executing plan for", key) - if data[key] is None: - print(f"skipping {key} because value is None") - data[key] = plan[key](data[key], bot) + for key, transform in plan.items(): + print("executing plan for", key) + data[key] = transform(data.get(key), bot) return cls._de_json(data=data, bot=bot, api_kwargs=api_kwargs) From 65777a737c88f9f65e1955e7ca5f7649a379b6c7 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Mon, 30 Mar 2026 05:16:04 -0700 Subject: [PATCH 06/11] Remove print statements for benchmark --- src/telegram/_telegramobject.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index b9a4e7f4bb5..23c6c9ae7f5 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -61,15 +61,15 @@ def _make_seq_transform( item_type: object, globalns: dict[str, object], tg_ns: dict[str, object], - cls_name: str, - field_name: str, + # cls_name: str, + # field_name: str, ) -> Any | None: """Recursively build a ``(value, bot) → transformed_value`` lambda for a Sequence item type. Returns :obj:`None` if the item type does not require any transformation. """ if isinstance(item_type, str): - print(f"Resolving forward reference for {cls_name}.{field_name}: {item_type}") + # print(f"Resolving forward reference for {cls_name}.{field_name}: {item_type}") item_type = eval(item_type, globalns, tg_ns) # noqa: S307 item_origin = get_origin(item_type) @@ -77,16 +77,17 @@ def _make_seq_transform( inner_args = get_args(item_type) if not inner_args: return None - inner_fn = _make_seq_transform(inner_args[0], globalns, tg_ns, cls_name, field_name) + # inner_fn = _make_seq_transform(inner_args[0], globalns, tg_ns, cls_name, field_name) + inner_fn = _make_seq_transform(inner_args[0], globalns, tg_ns) if inner_fn is None: return None - print(f"Adding nested sequence plan for {cls_name}.{field_name}") + # print(f"Adding nested sequence plan for {cls_name}.{field_name}") return lambda v, b, _f=inner_fn: ( () if v is None else [_f(row, b) for row in v] if isinstance(v, list) else v ) if isinstance(item_type, type) and issubclass(item_type, TelegramObject): - print(f"Adding de_list plan for {cls_name}.{field_name} → {item_type.__name__}") + # print(f"Adding de_list plan for {cls_name}.{field_name} → {item_type.__name__}") return lambda v, b, _c=item_type: ( () if v is None else _c.de_list(v, b) if isinstance(v, list) else v ) @@ -475,7 +476,7 @@ def _build_plan(cls) -> dict[str, Any]: if init_fn is None: # No own __init__: inherit the nearest ancestor's plan. This is true for e.g. Chat parent = cls.__mro__[1] - print("No __init__ for", cls.__name__, "inheriting from", parent.__name__) + # print("No __init__ for", cls.__name__, "inheriting from", parent.__name__) if "__DE_JSON_PLAN__" not in parent.__dict__: parent._build_plan() cls.__DE_JSON_PLAN__ = parent.__DE_JSON_PLAN__ @@ -508,7 +509,7 @@ def _build_plan(cls) -> dict[str, Any]: else from_timestamp(value, tzinfo=extract_tzinfo_from_defaults(bot)) ) elif isinstance(inner, type) and issubclass(inner, TelegramObject): - print("Adding de_json plan for", cls.__name__, name, "→", inner.__name__) + # print("Adding de_json plan for", cls.__name__, name, "→", inner.__name__) plan[name] = lambda v, b, _c=inner: ( None if v is None else v if isinstance(v, _c) else _c.de_json(v, b) ) @@ -516,11 +517,12 @@ def _build_plan(cls) -> dict[str, Any]: args = get_args(inner) if not args: continue - fn = _make_seq_transform(args[0], globalns, tg_ns, cls.__name__, name) + # fn = _make_seq_transform(args[0], globalns, tg_ns, cls.__name__, name) + fn = _make_seq_transform(args[0], globalns, tg_ns) if fn is not None: plan[name] = fn - else: - print(f"No de_json plan for {cls.__name__}.{name} (annotation: {ann})") + # else: + # print(f"No de_json plan for {cls.__name__}.{name} (annotation: {ann})") cls.__DE_JSON_PLAN__ = plan return plan @@ -590,14 +592,14 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel # Move removed/legacy API fields into api_kwargs api_kwargs: JSONDict | None = None if cls.__REMOVED_API_FIELDS__: - print(f"checking for removed API fields for {cls.__name__}") + # print(f"checking for removed API fields for {cls.__name__}") removed = {f: data.pop(f) for f in cls.__REMOVED_API_FIELDS__ if f in data} if removed: api_kwargs = removed # Make the plan for de_json. This is done only on the first call to de_json for each class. if "__DE_JSON_PLAN__" not in cls.__dict__: - print(f"building plan for {cls.__name__}") + # print(f"building plan for {cls.__name__}") cls._build_plan() plan = cls.__DE_JSON_PLAN__ @@ -609,9 +611,9 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel # TelegramObject → None, datetime → None, Sequence[TelegramObject] → (). # Iterating plan (not data) is O(plan size) and also handles missing keys correctly. if plan: - print(f"Data length: {len(data)}, plan length: {len(plan)}") + # print(f"Data length: {len(data)}, plan length: {len(plan)}") for key, transform in plan.items(): - print("executing plan for", key) + # print("executing plan for", key) data[key] = transform(data.get(key), bot) return cls._de_json(data=data, bot=bot, api_kwargs=api_kwargs) From 116aa08a937d4710202e9b2ffab7780dda35fb63 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Mon, 30 Mar 2026 06:29:33 -0700 Subject: [PATCH 07/11] WIP: Do some perf improvements --- src/telegram/_message.py | 4 +-- src/telegram/_telegramobject.py | 56 +++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/telegram/_message.py b/src/telegram/_message.py index 00c058530cd..becf5b56bf0 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -221,7 +221,7 @@ def _de_json( # Idempotent: the plan may have already converted these fields. date_val = data.get("date") if date_val is not None and not isinstance(date_val, dtm.datetime): - print("this was an int, converting to datetime in MaybeInaccessibleMessage") + # print("this was an int, converting to datetime in MaybeInaccessibleMessage") if date_val == 0: data["date"] = ZERO_DATE else: @@ -229,7 +229,7 @@ def _de_json( data["date"] = from_timestamp(date_val, tzinfo=loc_tzinfo) if not isinstance(data.get("chat"), Chat): - print("De-jsoning chat in MaybeInaccessibleMessage") + # print("De-jsoning chat in MaybeInaccessibleMessage") data["chat"] = de_json_optional(data.get("chat"), Chat, bot) return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index 23c6c9ae7f5..a02a196fd9c 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -538,12 +538,15 @@ def _de_json( try: obj = cls(**data, api_kwargs=api_kwargs) except TypeError as exc: - if "__init__() got an unexpected keyword argument" not in str(exc): + exc_str = str(exc) + if ( + "unexpected keyword argument" not in exc_str + and "required positional argument" not in exc_str + ): raise if cls.__INIT_PARAMS_CHECK is not cls: - signature = inspect.signature(cls) - cls.__INIT_PARAMS = set(signature.parameters.keys()) + cls.__INIT_PARAMS = set(inspect.signature(cls).parameters.keys()) cls.__INIT_PARAMS_CHECK = cls api_kwargs = api_kwargs or {} @@ -551,6 +554,14 @@ def _de_json( for key, value in data.items(): (existing_kwargs if key in cls.__INIT_PARAMS else api_kwargs)[key] = value + # Supply defaults for missing params using the plan's None-handling + # (Sequence → (), datetime/TelegramObject → None). Needed when + # data has both unexpected keys *and* is missing required fields + # (e.g. TransactionPartnerChat dispatched with sibling fields). + plan = cls.__dict__.get("__DE_JSON_PLAN__", {}) + for key in cls.__INIT_PARAMS - existing_kwargs.keys() - {"self", "api_kwargs"}: + existing_kwargs[key] = plan[key](None, bot) if key in plan else None + obj = cls(api_kwargs=api_kwargs, **existing_kwargs) obj.set_bot(bot=bot) @@ -572,11 +583,21 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel The Telegram object. """ + # Build the plan lazily (once per class). + if "__DE_JSON_PLAN__" not in cls.__dict__: + # print(f"building plan for {cls.__name__}") + cls._build_plan() + plan = cls.__DE_JSON_PLAN__ + + # Fast path: no dispatch, no removed fields, empty plan → skip data.copy() + if not plan and not cls.__DE_JSON_DISPATCH__ and not cls.__REMOVED_API_FIELDS__: + return cls._de_json(data=data, bot=bot) + + # We'll mutate data below (rename, pop, transform), so copy first. data = cls._parse_data(data) # Dispatch to subclass for delegator classes (e.g. TransactionPartner, ChatMember). - # Only dispatches when __DE_JSON_DISPATCH__ is defined directly on cls (not inherited). - if "__DE_JSON_DISPATCH__" in cls.__dict__: + if cls.__DE_JSON_DISPATCH__: dispatch_key, dispatch_mapping = cls.__DE_JSON_DISPATCH__ target_name = dispatch_mapping.get(data.get(dispatch_key)) if target_name is not None: @@ -597,24 +618,19 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel if removed: api_kwargs = removed - # Make the plan for de_json. This is done only on the first call to de_json for each class. - if "__DE_JSON_PLAN__" not in cls.__dict__: - # print(f"building plan for {cls.__name__}") - cls._build_plan() - plan = cls.__DE_JSON_PLAN__ - - # Rename "from" → "from_user" when the plan expects it and "from" is present in data. - if "from_user" in plan and "from" in data: - data["from_user"] = data.pop("from") + # Rename "from" → "from_user" before the transform loop. + if "from_user" in plan: + if "from" in data: + data["from_user"] = data.pop("from") + elif "from_user" not in data: + data["from_user"] = None - # Apply plan transforms for all plan keys. Lambdas handle None (missing/null) themselves: - # TelegramObject → None, datetime → None, Sequence[TelegramObject] → (). - # Iterating plan (not data) is O(plan size) and also handles missing keys correctly. if plan: # print(f"Data length: {len(data)}, plan length: {len(plan)}") - for key, transform in plan.items(): - # print("executing plan for", key) - data[key] = transform(data.get(key), bot) + for key in data: + if key in plan and data[key] is not None: + # print("executing plan for", key) + data[key] = plan[key](data[key], bot) return cls._de_json(data=data, bot=bot, api_kwargs=api_kwargs) From f49f9a7d02369293d79502b439b9aa091a39779f Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 31 Mar 2026 01:25:36 -0700 Subject: [PATCH 08/11] I'm happy enough with this implementation --- src/telegram/_telegramobject.py | 133 ++++++++++++++++---------------- 1 file changed, 68 insertions(+), 65 deletions(-) diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index a02a196fd9c..4b41e7012f3 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -30,6 +30,7 @@ from itertools import chain from types import MappingProxyType from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast, get_args, get_origin +from collections.abc import Callable from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp, to_timestamp from telegram._utils.defaultvalue import DefaultValue @@ -41,6 +42,7 @@ Tele_co = TypeVar("Tele_co", bound="TelegramObject", covariant=True) Tele = TypeVar("Tele", bound="TelegramObject") +_DATETIME_FIELD = object() # sentinel value to mark datetime fields in the de_json plan def _telegram_ns() -> dict[str, object]: @@ -61,15 +63,21 @@ def _make_seq_transform( item_type: object, globalns: dict[str, object], tg_ns: dict[str, object], - # cls_name: str, - # field_name: str, -) -> Any | None: +) -> "Callable[[object, Bot | None], object] | None": """Recursively build a ``(value, bot) → transformed_value`` lambda for a Sequence item type. - Returns :obj:`None` if the item type does not require any transformation. + Args: + item_type: The unwrapped type annotation of the items in the Sequence. + globalns: The global namespace for evaluating string annotations. + tg_ns: The telegram namespace for evaluating string annotations. + + Returns: + - :obj:`None` if the item type does not require any transformation. + - a lambda (value, bot) -> transformation """ + # inspect.signature does not resolve forward refs in Sequence's despite eval_str=True for some + # reason: if isinstance(item_type, str): - # print(f"Resolving forward reference for {cls_name}.{field_name}: {item_type}") item_type = eval(item_type, globalns, tg_ns) # noqa: S307 item_origin = get_origin(item_type) @@ -77,20 +85,13 @@ def _make_seq_transform( inner_args = get_args(item_type) if not inner_args: return None - # inner_fn = _make_seq_transform(inner_args[0], globalns, tg_ns, cls_name, field_name) inner_fn = _make_seq_transform(inner_args[0], globalns, tg_ns) if inner_fn is None: return None - # print(f"Adding nested sequence plan for {cls_name}.{field_name}") - return lambda v, b, _f=inner_fn: ( - () if v is None else [_f(row, b) for row in v] if isinstance(v, list) else v - ) + return lambda v, b, _f=inner_fn: [_f(row, b) for row in v] if isinstance(v, list) else v if isinstance(item_type, type) and issubclass(item_type, TelegramObject): - # print(f"Adding de_list plan for {cls_name}.{field_name} → {item_type.__name__}") - return lambda v, b, _c=item_type: ( - () if v is None else _c.de_list(v, b) if isinstance(v, list) else v - ) + return lambda v, b, _c=item_type: _c.de_list(v, b) if isinstance(v, list) else v return None @@ -142,11 +143,19 @@ class TelegramObject: # unless it's overridden __INIT_PARAMS_CHECK: type["TelegramObject"] | None = None - # Per-class declarative de_json plan. - # Maps parameter name → transform callable. Each subclass gets its own - # plan via _build_plan() + # Per-class de_json plan built once by _build_plan(). + # Maps parameter name → transform target: + # _DATETIME_FIELD sentinel → from_timestamp(value, tzinfo) + # TelegramObject subclass → cls.de_json(value, bot) + # callable(value, bot) → Sequence transform (from _make_seq_transform) __DE_JSON_PLAN__: ClassVar[dict[str, Any]] = {} + # Forward-compatibility: names of Sequence-typed __init__ params. + # If the Telegram API stops sending a field that PTB's __init__ still + # requires (i.e. the API made it optional), _de_json defaults sequences + # to () and everything else to None so the library doesn't crash. + __DE_JSON_COMPAT__: ClassVar[frozenset[str]] = frozenset() + # Subclasses may declare field names that Telegram still returns for backwards # compatibility but that are no longer part of the PTB model. Those fields will # be intercepted by de_json and forwarded into api_kwargs before construction. @@ -463,9 +472,8 @@ def _build_plan(cls) -> dict[str, Any]: Called once per class on the first :meth:`de_json` invocation. By that time every module is fully loaded so forward-reference resolution always succeeds. - The resulting ``dict`` maps parameter names (e.g. ``"from_user"``, - ``"date"``, ``"entities"``) to a lambda ``(value, bot) → converted_value``. - Three kinds of transforms are recognised: + The resulting ``dict`` maps parameter names to their transform targets. + Four kinds of transforms are recognised: * ``datetime`` fields → :func:`~telegram._utils.datetime.from_timestamp` * ``TelegramObject`` fields → ``TargetCls.de_json(...)`` @@ -476,19 +484,19 @@ def _build_plan(cls) -> dict[str, Any]: if init_fn is None: # No own __init__: inherit the nearest ancestor's plan. This is true for e.g. Chat parent = cls.__mro__[1] - # print("No __init__ for", cls.__name__, "inheriting from", parent.__name__) if "__DE_JSON_PLAN__" not in parent.__dict__: parent._build_plan() cls.__DE_JSON_PLAN__ = parent.__DE_JSON_PLAN__ + cls.__DE_JSON_COMPAT__ = parent.__DE_JSON_COMPAT__ return cls.__DE_JSON_PLAN__ plan: dict[str, Any] = {} + seq_fields: set[str] = set() globalns: dict[str, object] = getattr(init_fn, "__globals__", {}) tg_ns = _telegram_ns() + sig = inspect.signature(init_fn, eval_str=True, globals=globalns, locals=tg_ns) - for name, param in inspect.signature( - init_fn, eval_str=True, globals=globalns, locals=tg_ns - ).parameters.items(): + for name, param in sig.parameters.items(): if name in ("self", "api_kwargs") or param.kind in ( param.VAR_POSITIONAL, param.VAR_KEYWORD, @@ -498,33 +506,31 @@ def _build_plan(cls) -> dict[str, Any]: ann = param.annotation inner = _unwrap_optional(ann) origin = get_origin(inner) - # Possible optimization: extract tzinfo here once by making this accept `bot` argument. if inner is dtm.datetime: - plan[name] = lambda value, bot: ( - None - if value is None - else value - if isinstance(value, dtm.datetime) - else from_timestamp(value, tzinfo=extract_tzinfo_from_defaults(bot)) - ) + plan[name] = _DATETIME_FIELD elif isinstance(inner, type) and issubclass(inner, TelegramObject): - # print("Adding de_json plan for", cls.__name__, name, "→", inner.__name__) - plan[name] = lambda v, b, _c=inner: ( - None if v is None else v if isinstance(v, _c) else _c.de_json(v, b) - ) + plan[name] = inner elif origin is Sequence: args = get_args(inner) if not args: continue - # fn = _make_seq_transform(args[0], globalns, tg_ns, cls.__name__, name) fn = _make_seq_transform(args[0], globalns, tg_ns) if fn is not None: plan[name] = fn - # else: - # print(f"No de_json plan for {cls.__name__}.{name} (annotation: {ann})") + seq_fields.add(name) cls.__DE_JSON_PLAN__ = plan + cls.__DE_JSON_COMPAT__ = frozenset(seq_fields) + + # Pre-resolve any string class names in the dispatch mapping so that + # de_json() can look up the target class directly without string checks. + if cls.__DE_JSON_DISPATCH__: + _, dispatch_mapping = cls.__DE_JSON_DISPATCH__ + for key, value in dispatch_mapping.items(): + if isinstance(value, str): + dispatch_mapping[key] = tg_ns[value] + return plan @classmethod @@ -554,13 +560,11 @@ def _de_json( for key, value in data.items(): (existing_kwargs if key in cls.__INIT_PARAMS else api_kwargs)[key] = value - # Supply defaults for missing params using the plan's None-handling - # (Sequence → (), datetime/TelegramObject → None). Needed when - # data has both unexpected keys *and* is missing required fields - # (e.g. TransactionPartnerChat dispatched with sibling fields). - plan = cls.__dict__.get("__DE_JSON_PLAN__", {}) + # Forward-compat: if the API stopped sending a field PTB requires, + # default sequences to () and everything else to None. + compat = cls.__dict__.get("__DE_JSON_COMPAT__", frozenset()) for key in cls.__INIT_PARAMS - existing_kwargs.keys() - {"self", "api_kwargs"}: - existing_kwargs[key] = plan[key](None, bot) if key in plan else None + existing_kwargs[key] = () if key in compat else None obj = cls(api_kwargs=api_kwargs, **existing_kwargs) @@ -585,7 +589,6 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel """ # Build the plan lazily (once per class). if "__DE_JSON_PLAN__" not in cls.__dict__: - # print(f"building plan for {cls.__name__}") cls._build_plan() plan = cls.__DE_JSON_PLAN__ @@ -599,38 +602,38 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel # Dispatch to subclass for delegator classes (e.g. TransactionPartner, ChatMember). if cls.__DE_JSON_DISPATCH__: dispatch_key, dispatch_mapping = cls.__DE_JSON_DISPATCH__ - target_name = dispatch_mapping.get(data.get(dispatch_key)) - if target_name is not None: - # Resolve string class name → actual class (lazy, cached) - if isinstance(target_name, str): - target_cls = _telegram_ns()[target_name] - dispatch_mapping[data[dispatch_key]] = target_cls - else: - target_cls = target_name + target_cls: Tele_co = dispatch_mapping.get(data.get(dispatch_key)) + if target_cls is not None: data.pop(dispatch_key) return target_cls.de_json(data=data, bot=bot) # Move removed/legacy API fields into api_kwargs api_kwargs: JSONDict | None = None if cls.__REMOVED_API_FIELDS__: - # print(f"checking for removed API fields for {cls.__name__}") removed = {f: data.pop(f) for f in cls.__REMOVED_API_FIELDS__ if f in data} if removed: api_kwargs = removed # Rename "from" → "from_user" before the transform loop. - if "from_user" in plan: - if "from" in data: - data["from_user"] = data.pop("from") - elif "from_user" not in data: - data["from_user"] = None + if "from_user" in plan and "from" in data: + data["from_user"] = data.pop("from") + # Let's finally apply the transformations: if plan: - # print(f"Data length: {len(data)}, plan length: {len(plan)}") - for key in data: - if key in plan and data[key] is not None: - # print("executing plan for", key) - data[key] = plan[key](data[key], bot) + # Compute tzinfo once for all datetime fields, if any + tz = extract_tzinfo_from_defaults(bot) + for key in data: # Only loop through keys returned by the API + if key in plan and data[key] is not None: # Should we transform this field? + target = plan[key] # The transform target for this field + if target is _DATETIME_FIELD: # timestamp → datetime + if not isinstance(data[key], dtm.datetime): # Avoid retransformations + data[key] = from_timestamp(data[key], tzinfo=tz) + elif isinstance(target, type): # Target is a TelegramObject subclass → de_json + if not isinstance(data[key], target): # Avoid retransformations + data[key] = target.de_json(data[key], bot) + else: + # Sequence transform callable (e.g. de_list) + data[key] = target(data[key], bot) return cls._de_json(data=data, bot=bot, api_kwargs=api_kwargs) From 42dcd86fd21be05c50c452927f8e4aaab7c230d3 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 31 Mar 2026 02:01:32 -0700 Subject: [PATCH 09/11] Run ruff formatter and linter --- src/telegram/_botcommandscope.py | 5 +--- src/telegram/_business.py | 9 ++----- src/telegram/_callbackquery.py | 2 -- src/telegram/_chatbackground.py | 5 +--- src/telegram/_chatboost.py | 5 +--- src/telegram/_chatfullinfo.py | 6 +---- src/telegram/_chatinvitelink.py | 6 +---- src/telegram/_chatjoinrequest.py | 6 ----- src/telegram/_chatlocation.py | 5 +--- src/telegram/_chatmember.py | 5 +--- src/telegram/_chatmemberupdated.py | 6 ----- src/telegram/_chatowner.py | 5 ---- src/telegram/_chatpermissions.py | 5 ---- src/telegram/_checklists.py | 2 +- src/telegram/_choseninlineresult.py | 3 +-- src/telegram/_directmessagestopic.py | 5 ---- src/telegram/_files/sticker.py | 5 ++-- src/telegram/_files/venue.py | 5 ---- src/telegram/_files/video.py | 3 +-- src/telegram/_games/game.py | 6 +---- src/telegram/_games/gamehighscore.py | 5 ---- src/telegram/_gifts.py | 6 +---- src/telegram/_inline/inlinekeyboardbutton.py | 5 +--- src/telegram/_inline/inlinekeyboardmarkup.py | 4 --- src/telegram/_inline/inlinequery.py | 4 +-- .../_inline/inlinequeryresultsbutton.py | 5 +--- .../_inline/inputinvoicemessagecontent.py | 6 +---- src/telegram/_inline/preparedinlinemessage.py | 4 --- src/telegram/_keyboardbutton.py | 6 ----- src/telegram/_keyboardbuttonrequest.py | 6 ----- src/telegram/_menubutton.py | 5 +--- src/telegram/_messageentity.py | 5 +--- src/telegram/_messageorigin.py | 5 +--- src/telegram/_messagereactionupdated.py | 6 +---- src/telegram/_ownedgift.py | 5 +--- src/telegram/_paidmedia.py | 5 ++-- src/telegram/_passport/credentials.py | 7 ++---- src/telegram/_passport/passportdata.py | 4 +-- src/telegram/_payment/orderinfo.py | 5 ---- src/telegram/_payment/precheckoutquery.py | 5 ---- src/telegram/_payment/shippingquery.py | 1 - src/telegram/_payment/stars/affiliateinfo.py | 5 ++-- .../_payment/stars/revenuewithdrawalstate.py | 5 +--- .../_payment/stars/startransactions.py | 6 +---- .../_payment/stars/transactionpartner.py | 6 ++--- src/telegram/_payment/successfulpayment.py | 4 --- src/telegram/_poll.py | 6 +---- src/telegram/_proximityalerttriggered.py | 5 ---- src/telegram/_reaction.py | 5 +--- src/telegram/_reply.py | 7 +++--- src/telegram/_shared.py | 6 +---- src/telegram/_story.py | 5 ---- src/telegram/_suggestedpost.py | 5 +--- src/telegram/_telegramobject.py | 25 ++++++++++--------- src/telegram/_uniquegift.py | 7 ++---- src/telegram/_update.py | 2 +- src/telegram/_userprofileaudios.py | 4 --- src/telegram/_userprofilephotos.py | 4 --- src/telegram/_videochat.py | 4 --- src/telegram/_webhookinfo.py | 4 --- 60 files changed, 62 insertions(+), 256 deletions(-) diff --git a/src/telegram/_botcommandscope.py b/src/telegram/_botcommandscope.py index 3a4b9077af2..44aa70389d5 100644 --- a/src/telegram/_botcommandscope.py +++ b/src/telegram/_botcommandscope.py @@ -19,16 +19,13 @@ # pylint: disable=redefined-builtin """This module contains objects representing Telegram bot command scopes.""" -from typing import TYPE_CHECKING, ClassVar, Final +from typing import ClassVar, Final from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class BotCommandScope(TelegramObject): """Base class for objects that represent the scope to which bot commands are applied. diff --git a/src/telegram/_business.py b/src/telegram/_business.py index b7861b8c503..58b619465be 100644 --- a/src/telegram/_business.py +++ b/src/telegram/_business.py @@ -25,24 +25,19 @@ from zoneinfo import ZoneInfo from telegram._chat import Chat -from telegram._files.location import Location from telegram._files.sticker import Sticker from telegram._telegramobject import TelegramObject -from telegram._user import User from telegram._utils.argumentparsing import ( - de_json_optional, - de_list_optional, parse_sequence_arg, ) from telegram._utils.datetime import ( - extract_tzinfo_from_defaults, - from_timestamp, get_zone_info, ) from telegram._utils.types import JSONDict if TYPE_CHECKING: - from telegram import Bot + from telegram._files.location import Location + from telegram._user import User class BusinessBotRights(TelegramObject): diff --git a/src/telegram/_callbackquery.py b/src/telegram/_callbackquery.py index 9cabb5daf88..38ce19814c0 100644 --- a/src/telegram/_callbackquery.py +++ b/src/telegram/_callbackquery.py @@ -27,13 +27,11 @@ from telegram._message import MaybeInaccessibleMessage, Message from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput, TimePeriod if TYPE_CHECKING: from telegram import ( - Bot, GameHighScore, InlineKeyboardMarkup, InputMedia, diff --git a/src/telegram/_chatbackground.py b/src/telegram/_chatbackground.py index 8f4ed17547a..fe833b0f518 100644 --- a/src/telegram/_chatbackground.py +++ b/src/telegram/_chatbackground.py @@ -19,7 +19,7 @@ """This module contains objects related to chat backgrounds.""" from collections.abc import Sequence -from typing import TYPE_CHECKING, ClassVar, Final +from typing import ClassVar, Final from telegram import constants from telegram._files.document import Document @@ -28,9 +28,6 @@ from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class BackgroundFill(TelegramObject): """Base class for Telegram BackgroundFill Objects. It can be one of: diff --git a/src/telegram/_chatboost.py b/src/telegram/_chatboost.py index c1be871bc8f..4889f9c9ec8 100644 --- a/src/telegram/_chatboost.py +++ b/src/telegram/_chatboost.py @@ -20,7 +20,7 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING, ClassVar, Final +from typing import ClassVar, Final from telegram import constants from telegram._chat import Chat @@ -30,9 +30,6 @@ from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class ChatBoostAdded(TelegramObject): """ diff --git a/src/telegram/_chatfullinfo.py b/src/telegram/_chatfullinfo.py index 9fd551546b9..7404a4c513a 100644 --- a/src/telegram/_chatfullinfo.py +++ b/src/telegram/_chatfullinfo.py @@ -34,20 +34,16 @@ from telegram._uniquegift import UniqueGiftColors from telegram._userrating import UserRating from telegram._utils.argumentparsing import ( - de_json_optional, - de_list_optional, parse_sequence_arg, to_timedelta, ) from telegram._utils.datetime import ( - extract_tzinfo_from_defaults, - from_timestamp, get_timedelta_value, ) from telegram._utils.types import JSONDict, TimePeriod if TYPE_CHECKING: - from telegram import Bot, BusinessIntro, BusinessLocation, BusinessOpeningHours, Message + from telegram import BusinessIntro, BusinessLocation, BusinessOpeningHours, Message class ChatFullInfo(_ChatBase): diff --git a/src/telegram/_chatinvitelink.py b/src/telegram/_chatinvitelink.py index cbb1be6c188..f107f585904 100644 --- a/src/telegram/_chatinvitelink.py +++ b/src/telegram/_chatinvitelink.py @@ -19,17 +19,13 @@ """This module contains an object that represents an invite link for a chat.""" import datetime as dtm -from typing import TYPE_CHECKING from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional, to_timedelta +from telegram._utils.argumentparsing import to_timedelta from telegram._utils.datetime import get_timedelta_value from telegram._utils.types import JSONDict, TimePeriod -if TYPE_CHECKING: - from telegram import Bot - class ChatInviteLink(TelegramObject): """This object represents an invite link for a chat. diff --git a/src/telegram/_chatjoinrequest.py b/src/telegram/_chatjoinrequest.py index 7defa6f1689..16c72609205 100644 --- a/src/telegram/_chatjoinrequest.py +++ b/src/telegram/_chatjoinrequest.py @@ -19,20 +19,14 @@ """This module contains an object that represents a Telegram ChatJoinRequest.""" import datetime as dtm -from typing import TYPE_CHECKING from telegram._chat import Chat from telegram._chatinvitelink import ChatInviteLink from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional -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 -if TYPE_CHECKING: - from telegram import Bot - class ChatJoinRequest(TelegramObject): """This object represents a join request sent to a chat. diff --git a/src/telegram/_chatlocation.py b/src/telegram/_chatlocation.py index 0788341fe1f..75afd1db57b 100644 --- a/src/telegram/_chatlocation.py +++ b/src/telegram/_chatlocation.py @@ -18,16 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a location to which a chat is connected.""" -from typing import TYPE_CHECKING, Final +from typing import Final from telegram import constants from telegram._files.location import Location from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class ChatLocation(TelegramObject): """This object represents a location to which a chat is connected. diff --git a/src/telegram/_chatmember.py b/src/telegram/_chatmember.py index 2ee76154444..ae67028bc2c 100644 --- a/src/telegram/_chatmember.py +++ b/src/telegram/_chatmember.py @@ -19,7 +19,7 @@ """This module contains an object that represents a Telegram ChatMember.""" import datetime as dtm -from typing import TYPE_CHECKING, ClassVar, Final +from typing import ClassVar, Final from telegram import constants from telegram._telegramobject import TelegramObject @@ -27,9 +27,6 @@ from telegram._utils import enum from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class ChatMember(TelegramObject): """Base class for Telegram ChatMember Objects. diff --git a/src/telegram/_chatmemberupdated.py b/src/telegram/_chatmemberupdated.py index 98c4b9b7088..640292b9b8d 100644 --- a/src/telegram/_chatmemberupdated.py +++ b/src/telegram/_chatmemberupdated.py @@ -19,20 +19,14 @@ """This module contains an object that represents a Telegram ChatMemberUpdated.""" import datetime as dtm -from typing import TYPE_CHECKING from telegram._chat import Chat from telegram._chatinvitelink import ChatInviteLink from telegram._chatmember import ChatMember from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class ChatMemberUpdated(TelegramObject): """This object represents changes in the status of a chat member. diff --git a/src/telegram/_chatowner.py b/src/telegram/_chatowner.py index e152082bb4e..fd8c4608bd6 100644 --- a/src/telegram/_chatowner.py +++ b/src/telegram/_chatowner.py @@ -18,15 +18,10 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a chat owner change in the chat.""" -from typing import TYPE_CHECKING - from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class ChatOwnerChanged(TelegramObject): """This object represents a service message about an ownership change in the chat. diff --git a/src/telegram/_chatpermissions.py b/src/telegram/_chatpermissions.py index 6fd8b134f66..9aa0129a13c 100644 --- a/src/telegram/_chatpermissions.py +++ b/src/telegram/_chatpermissions.py @@ -18,14 +18,9 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ChatPermission.""" -from typing import TYPE_CHECKING - from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class ChatPermissions(TelegramObject): """Describes actions that a non-administrator user is allowed to take in a chat. diff --git a/src/telegram/_checklists.py b/src/telegram/_checklists.py index aab0d7752d0..a4b94022022 100644 --- a/src/telegram/_checklists.py +++ b/src/telegram/_checklists.py @@ -20,7 +20,7 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from telegram._chat import Chat from telegram._messageentity import MessageEntity diff --git a/src/telegram/_choseninlineresult.py b/src/telegram/_choseninlineresult.py index 8e8d26f7a55..8f8b733cf81 100644 --- a/src/telegram/_choseninlineresult.py +++ b/src/telegram/_choseninlineresult.py @@ -21,13 +21,12 @@ from typing import TYPE_CHECKING -from telegram._files.location import Location from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.types import JSONDict if TYPE_CHECKING: - from telegram import Bot + from telegram._files.location import Location class ChosenInlineResult(TelegramObject): diff --git a/src/telegram/_directmessagestopic.py b/src/telegram/_directmessagestopic.py index 11e05fa1e22..4aeee4f2dda 100644 --- a/src/telegram/_directmessagestopic.py +++ b/src/telegram/_directmessagestopic.py @@ -18,15 +18,10 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the DirectMessagesTopic class.""" -from typing import TYPE_CHECKING, Optional - from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram._bot import Bot - class DirectMessagesTopic(TelegramObject): """ diff --git a/src/telegram/_files/sticker.py b/src/telegram/_files/sticker.py index 84abf43e40b..9a32eb34f1b 100644 --- a/src/telegram/_files/sticker.py +++ b/src/telegram/_files/sticker.py @@ -23,15 +23,14 @@ from telegram import constants from telegram._files._basethumbedmedium import _BaseThumbedMedium -from telegram._files.file import File from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: - from telegram import Bot + from telegram._files.file import File class Sticker(_BaseThumbedMedium): diff --git a/src/telegram/_files/venue.py b/src/telegram/_files/venue.py index f9ee3db9ae2..5b2ab2ff557 100644 --- a/src/telegram/_files/venue.py +++ b/src/telegram/_files/venue.py @@ -18,15 +18,10 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Venue.""" -from typing import TYPE_CHECKING - from telegram._files.location import Location from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class Venue(TelegramObject): """This object represents a venue. diff --git a/src/telegram/_files/video.py b/src/telegram/_files/video.py index e07068794a8..3b0b99f6e73 100644 --- a/src/telegram/_files/video.py +++ b/src/telegram/_files/video.py @@ -20,12 +20,11 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._files._basethumbedmedium import _BaseThumbedMedium from telegram._files.photosize import PhotoSize from telegram._files.videoquality import VideoQuality -from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg, to_timedelta +from telegram._utils.argumentparsing import parse_sequence_arg, to_timedelta from telegram._utils.datetime import get_timedelta_value from telegram._utils.types import JSONDict, TimePeriod diff --git a/src/telegram/_games/game.py b/src/telegram/_games/game.py index b1b6fc0254b..7135628f501 100644 --- a/src/telegram/_games/game.py +++ b/src/telegram/_games/game.py @@ -19,19 +19,15 @@ """This module contains an object that represents a Telegram Game.""" from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._files.animation import Animation from telegram._files.photosize import PhotoSize from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.strings import TextEncoding from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class Game(TelegramObject): """ diff --git a/src/telegram/_games/gamehighscore.py b/src/telegram/_games/gamehighscore.py index e0159b9ea97..3b13a2ea887 100644 --- a/src/telegram/_games/gamehighscore.py +++ b/src/telegram/_games/gamehighscore.py @@ -18,15 +18,10 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram GameHighScore.""" -from typing import TYPE_CHECKING - from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class GameHighScore(TelegramObject): """This object represents one row of the high scores table for a game. diff --git a/src/telegram/_gifts.py b/src/telegram/_gifts.py index bb988f86777..ad843b336b6 100644 --- a/src/telegram/_gifts.py +++ b/src/telegram/_gifts.py @@ -20,19 +20,15 @@ """This module contains classes related to gifs sent by bots.""" from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._chat import Chat from telegram._files.sticker import Sticker from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.entities import parse_message_entities, parse_message_entity from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class GiftBackground(TelegramObject): """This object describes the background of a gift. diff --git a/src/telegram/_inline/inlinekeyboardbutton.py b/src/telegram/_inline/inlinekeyboardbutton.py index 6d73d76fdbb..9a3cf7e062b 100644 --- a/src/telegram/_inline/inlinekeyboardbutton.py +++ b/src/telegram/_inline/inlinekeyboardbutton.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram InlineKeyboardButton.""" -from typing import TYPE_CHECKING, Final +from typing import Final from telegram import constants from telegram._copytextbutton import CopyTextButton @@ -29,9 +29,6 @@ from telegram._utils.types import JSONDict from telegram._webappinfo import WebAppInfo -if TYPE_CHECKING: - from telegram import Bot - class InlineKeyboardButton(TelegramObject): """This object represents one button of an inline keyboard. diff --git a/src/telegram/_inline/inlinekeyboardmarkup.py b/src/telegram/_inline/inlinekeyboardmarkup.py index 540a84ce6e5..a3162eae0b4 100644 --- a/src/telegram/_inline/inlinekeyboardmarkup.py +++ b/src/telegram/_inline/inlinekeyboardmarkup.py @@ -19,16 +19,12 @@ """This module contains an object that represents a Telegram InlineKeyboardMarkup.""" from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardbutton import InlineKeyboardButton from telegram._telegramobject import TelegramObject from telegram._utils.markup import check_keyboard_type from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class InlineKeyboardMarkup(TelegramObject): """ diff --git a/src/telegram/_inline/inlinequery.py b/src/telegram/_inline/inlinequery.py index eb83840826b..b825059d667 100644 --- a/src/telegram/_inline/inlinequery.py +++ b/src/telegram/_inline/inlinequery.py @@ -23,7 +23,6 @@ from typing import TYPE_CHECKING, Final from telegram import constants -from telegram._files.location import Location from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton from telegram._telegramobject import TelegramObject from telegram._user import User @@ -31,7 +30,8 @@ from telegram._utils.types import JSONDict, ODVInput, TimePeriod if TYPE_CHECKING: - from telegram import Bot, InlineQueryResult + from telegram import InlineQueryResult + from telegram._files.location import Location class InlineQuery(TelegramObject): diff --git a/src/telegram/_inline/inlinequeryresultsbutton.py b/src/telegram/_inline/inlinequeryresultsbutton.py index 5f06f858f3c..eaa065f8334 100644 --- a/src/telegram/_inline/inlinequeryresultsbutton.py +++ b/src/telegram/_inline/inlinequeryresultsbutton.py @@ -18,16 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the class that represent a Telegram InlineQueryResultsButton.""" -from typing import TYPE_CHECKING, Final +from typing import Final from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict from telegram._webappinfo import WebAppInfo -if TYPE_CHECKING: - from telegram import Bot - class InlineQueryResultsButton(TelegramObject): """This object represents a button to be shown above inline query results. You **must** use diff --git a/src/telegram/_inline/inputinvoicemessagecontent.py b/src/telegram/_inline/inputinvoicemessagecontent.py index 87638bf405a..6eec6479d85 100644 --- a/src/telegram/_inline/inputinvoicemessagecontent.py +++ b/src/telegram/_inline/inputinvoicemessagecontent.py @@ -19,16 +19,12 @@ """This module contains a class that represents a Telegram InputInvoiceMessageContent.""" from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._inline.inputmessagecontent import InputMessageContent from telegram._payment.labeledprice import LabeledPrice -from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class InputInvoiceMessageContent(InputMessageContent): """ diff --git a/src/telegram/_inline/preparedinlinemessage.py b/src/telegram/_inline/preparedinlinemessage.py index 73e858a95c5..fb03082f1f7 100644 --- a/src/telegram/_inline/preparedinlinemessage.py +++ b/src/telegram/_inline/preparedinlinemessage.py @@ -19,14 +19,10 @@ """This module contains an object that represents a Telegram Prepared inline Message.""" import datetime as dtm -from typing import TYPE_CHECKING from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class PreparedInlineMessage(TelegramObject): """Describes an inline message to be sent by a user of a Mini App. diff --git a/src/telegram/_keyboardbutton.py b/src/telegram/_keyboardbutton.py index 109b3ca0414..91c3394424e 100644 --- a/src/telegram/_keyboardbutton.py +++ b/src/telegram/_keyboardbutton.py @@ -18,18 +18,12 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram KeyboardButton.""" -from typing import TYPE_CHECKING - from telegram._keyboardbuttonpolltype import KeyboardButtonPollType from telegram._keyboardbuttonrequest import KeyboardButtonRequestChat, KeyboardButtonRequestUsers from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict from telegram._webappinfo import WebAppInfo -if TYPE_CHECKING: - from telegram import Bot - class KeyboardButton(TelegramObject): """ diff --git a/src/telegram/_keyboardbuttonrequest.py b/src/telegram/_keyboardbuttonrequest.py index 457a4ca015b..7eac5677087 100644 --- a/src/telegram/_keyboardbuttonrequest.py +++ b/src/telegram/_keyboardbuttonrequest.py @@ -18,16 +18,10 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains two objects to request chats/users.""" -from typing import TYPE_CHECKING - from telegram._chatadministratorrights import ChatAdministratorRights from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class KeyboardButtonRequestUsers(TelegramObject): """This object defines the criteria used to request a suitable user. The identifier of the diff --git a/src/telegram/_menubutton.py b/src/telegram/_menubutton.py index 92a8ecf88c5..c7615587496 100644 --- a/src/telegram/_menubutton.py +++ b/src/telegram/_menubutton.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains objects related to Telegram menu buttons.""" -from typing import TYPE_CHECKING, ClassVar, Final +from typing import ClassVar, Final from telegram import constants from telegram._telegramobject import TelegramObject @@ -26,9 +26,6 @@ from telegram._utils.types import JSONDict from telegram._webappinfo import WebAppInfo -if TYPE_CHECKING: - from telegram import Bot - class MenuButton(TelegramObject): """This object describes the bot's menu button in a private chat. It should be one of diff --git a/src/telegram/_messageentity.py b/src/telegram/_messageentity.py index bf46ddcbc1f..62cf83e28f3 100644 --- a/src/telegram/_messageentity.py +++ b/src/telegram/_messageentity.py @@ -22,7 +22,7 @@ import datetime as dtm import itertools from collections.abc import Sequence -from typing import TYPE_CHECKING, Final +from typing import Final from telegram import constants from telegram._telegramobject import TelegramObject @@ -31,9 +31,6 @@ from telegram._utils.strings import TextEncoding from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - _SEM = Sequence["MessageEntity"] diff --git a/src/telegram/_messageorigin.py b/src/telegram/_messageorigin.py index 3010e0aefab..63e22487721 100644 --- a/src/telegram/_messageorigin.py +++ b/src/telegram/_messageorigin.py @@ -19,7 +19,7 @@ """This module contains the classes that represent Telegram MessageOigin.""" import datetime as dtm -from typing import TYPE_CHECKING, ClassVar, Final +from typing import ClassVar, Final from telegram import constants from telegram._chat import Chat @@ -28,9 +28,6 @@ from telegram._utils import enum from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class MessageOrigin(TelegramObject): """ diff --git a/src/telegram/_messagereactionupdated.py b/src/telegram/_messagereactionupdated.py index d2dd3293777..b2e0f16421a 100644 --- a/src/telegram/_messagereactionupdated.py +++ b/src/telegram/_messagereactionupdated.py @@ -20,18 +20,14 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._chat import Chat from telegram._reaction import ReactionCount, ReactionType from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class MessageReactionCountUpdated(TelegramObject): """This class represents reaction changes on a message with anonymous reactions. diff --git a/src/telegram/_ownedgift.py b/src/telegram/_ownedgift.py index 685ec96472a..feee77383dd 100644 --- a/src/telegram/_ownedgift.py +++ b/src/telegram/_ownedgift.py @@ -20,7 +20,7 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING, ClassVar, Final +from typing import ClassVar, Final from telegram import constants from telegram._gifts import Gift @@ -33,9 +33,6 @@ from telegram._utils.entities import parse_message_entities, parse_message_entity from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class OwnedGift(TelegramObject): """This object describes a gift received and owned by a user or a chat. Currently, it diff --git a/src/telegram/_paidmedia.py b/src/telegram/_paidmedia.py index 97553fb4ef7..6a03b8a56c1 100644 --- a/src/telegram/_paidmedia.py +++ b/src/telegram/_paidmedia.py @@ -23,10 +23,8 @@ from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants -from telegram._files.photosize import PhotoSize from telegram._files.video import Video from telegram._telegramobject import TelegramObject -from telegram._user import User from telegram._utils import enum from telegram._utils.argumentparsing import ( parse_sequence_arg, @@ -36,7 +34,8 @@ from telegram._utils.types import JSONDict, TimePeriod if TYPE_CHECKING: - from telegram import Bot + from telegram._files.photosize import PhotoSize + from telegram._user import User class PaidMedia(TelegramObject): diff --git a/src/telegram/_passport/credentials.py b/src/telegram/_passport/credentials.py index ff754a6b270..6252ffba0a0 100644 --- a/src/telegram/_passport/credentials.py +++ b/src/telegram/_passport/credentials.py @@ -20,7 +20,7 @@ import json from base64 import b64decode from collections.abc import Sequence -from typing import TYPE_CHECKING, no_type_check +from typing import no_type_check try: from cryptography.hazmat.backends import default_backend @@ -39,14 +39,11 @@ CRYPTO_INSTALLED = False from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.strings import TextEncoding from telegram._utils.types import JSONDict from telegram.error import PassportDecryptionError -if TYPE_CHECKING: - from telegram import Bot - @no_type_check def decrypt(secret, hash, data): diff --git a/src/telegram/_passport/passportdata.py b/src/telegram/_passport/passportdata.py index 76088709dbb..73bf54976ba 100644 --- a/src/telegram/_passport/passportdata.py +++ b/src/telegram/_passport/passportdata.py @@ -24,11 +24,11 @@ from telegram._passport.credentials import EncryptedCredentials from telegram._passport.encryptedpassportelement import EncryptedPassportElement from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: - from telegram import Bot, Credentials + from telegram import Credentials class PassportData(TelegramObject): diff --git a/src/telegram/_payment/orderinfo.py b/src/telegram/_payment/orderinfo.py index fd73e67704b..87997199b6c 100644 --- a/src/telegram/_payment/orderinfo.py +++ b/src/telegram/_payment/orderinfo.py @@ -18,15 +18,10 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram OrderInfo.""" -from typing import TYPE_CHECKING - from telegram._payment.shippingaddress import ShippingAddress from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class OrderInfo(TelegramObject): """This object represents information about an order. diff --git a/src/telegram/_payment/precheckoutquery.py b/src/telegram/_payment/precheckoutquery.py index edb1f2ab413..096e3954d7d 100644 --- a/src/telegram/_payment/precheckoutquery.py +++ b/src/telegram/_payment/precheckoutquery.py @@ -18,17 +18,12 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram PreCheckoutQuery.""" -from typing import TYPE_CHECKING - from telegram._payment.orderinfo import OrderInfo from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput -if TYPE_CHECKING: - from telegram import Bot - class PreCheckoutQuery(TelegramObject): """This object contains information about an incoming pre-checkout query. diff --git a/src/telegram/_payment/shippingquery.py b/src/telegram/_payment/shippingquery.py index c5f34ec4ab3..fcbd45fad13 100644 --- a/src/telegram/_payment/shippingquery.py +++ b/src/telegram/_payment/shippingquery.py @@ -28,7 +28,6 @@ from telegram._utils.types import JSONDict, ODVInput if TYPE_CHECKING: - from telegram import Bot from telegram._payment.shippingoption import ShippingOption diff --git a/src/telegram/_payment/stars/affiliateinfo.py b/src/telegram/_payment/stars/affiliateinfo.py index 99fb6ec6205..862e3584705 100644 --- a/src/telegram/_payment/stars/affiliateinfo.py +++ b/src/telegram/_payment/stars/affiliateinfo.py @@ -20,13 +20,12 @@ from typing import TYPE_CHECKING -from telegram._chat import Chat from telegram._telegramobject import TelegramObject -from telegram._user import User from telegram._utils.types import JSONDict if TYPE_CHECKING: - from telegram import Bot + from telegram._chat import Chat + from telegram._user import User class AffiliateInfo(TelegramObject): diff --git a/src/telegram/_payment/stars/revenuewithdrawalstate.py b/src/telegram/_payment/stars/revenuewithdrawalstate.py index 83199b85579..a53e49f530f 100644 --- a/src/telegram/_payment/stars/revenuewithdrawalstate.py +++ b/src/telegram/_payment/stars/revenuewithdrawalstate.py @@ -20,16 +20,13 @@ """This module contains the classes for Telegram Stars Revenue Withdrawals.""" import datetime as dtm -from typing import TYPE_CHECKING, ClassVar, Final +from typing import ClassVar, Final from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class RevenueWithdrawalState(TelegramObject): """This object describes the state of a revenue withdrawal operation. Currently, it can be one diff --git a/src/telegram/_payment/stars/startransactions.py b/src/telegram/_payment/stars/startransactions.py index 36f60a34285..d47c48291de 100644 --- a/src/telegram/_payment/stars/startransactions.py +++ b/src/telegram/_payment/stars/startransactions.py @@ -21,17 +21,13 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict from .transactionpartner import TransactionPartner -if TYPE_CHECKING: - from telegram import Bot - class StarTransaction(TelegramObject): """Describes a Telegram Star transaction. diff --git a/src/telegram/_payment/stars/transactionpartner.py b/src/telegram/_payment/stars/transactionpartner.py index ab0142c4da1..d6069204f5e 100644 --- a/src/telegram/_payment/stars/transactionpartner.py +++ b/src/telegram/_payment/stars/transactionpartner.py @@ -27,7 +27,6 @@ from telegram._gifts import Gift from telegram._paidmedia import PaidMedia from telegram._telegramobject import TelegramObject -from telegram._user import User from telegram._utils import enum from telegram._utils.argumentparsing import ( parse_sequence_arg, @@ -36,12 +35,13 @@ from telegram._utils.types import JSONDict, TimePeriod from .affiliateinfo import AffiliateInfo -from .revenuewithdrawalstate import RevenueWithdrawalState if TYPE_CHECKING: import datetime as dtm - from telegram import Bot + from telegram._user import User + + from .revenuewithdrawalstate import RevenueWithdrawalState class TransactionPartner(TelegramObject): diff --git a/src/telegram/_payment/successfulpayment.py b/src/telegram/_payment/successfulpayment.py index fad4e6eed1e..8bf354330e4 100644 --- a/src/telegram/_payment/successfulpayment.py +++ b/src/telegram/_payment/successfulpayment.py @@ -19,15 +19,11 @@ """This module contains an object that represents a Telegram SuccessfulPayment.""" import datetime as dtm -from typing import TYPE_CHECKING from telegram._payment.orderinfo import OrderInfo from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class SuccessfulPayment(TelegramObject): """This object contains basic information about a successful payment. diff --git a/src/telegram/_poll.py b/src/telegram/_poll.py index 99ea05d96e7..3fdf0733c10 100644 --- a/src/telegram/_poll.py +++ b/src/telegram/_poll.py @@ -20,7 +20,7 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING, Final +from typing import Final from telegram import constants from telegram._chat import Chat @@ -29,7 +29,6 @@ from telegram._user import User from telegram._utils import enum from telegram._utils.argumentparsing import ( - de_list_optional, parse_sequence_arg, to_timedelta, ) @@ -38,9 +37,6 @@ from telegram._utils.entities import parse_message_entities, parse_message_entity from telegram._utils.types import JSONDict, ODVInput, TimePeriod -if TYPE_CHECKING: - from telegram import Bot - class InputPollOption(TelegramObject): """ diff --git a/src/telegram/_proximityalerttriggered.py b/src/telegram/_proximityalerttriggered.py index 2e9678f322f..4907172ab71 100644 --- a/src/telegram/_proximityalerttriggered.py +++ b/src/telegram/_proximityalerttriggered.py @@ -18,15 +18,10 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Proximity Alert.""" -from typing import TYPE_CHECKING - from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class ProximityAlertTriggered(TelegramObject): """ diff --git a/src/telegram/_reaction.py b/src/telegram/_reaction.py index adf8b219cf0..1ae97580e62 100644 --- a/src/telegram/_reaction.py +++ b/src/telegram/_reaction.py @@ -19,16 +19,13 @@ # pylint: disable=redefined-builtin """This module contains objects that represents a Telegram ReactionType.""" -from typing import TYPE_CHECKING, ClassVar, Final, Literal +from typing import ClassVar, Final, Literal from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class ReactionType(TelegramObject): """Base class for Telegram ReactionType Objects. diff --git a/src/telegram/_reply.py b/src/telegram/_reply.py index 010a304e46e..c9ff0121945 100644 --- a/src/telegram/_reply.py +++ b/src/telegram/_reply.py @@ -26,9 +26,7 @@ from telegram._dice import Dice from telegram._files.animation import Animation from telegram._files.audio import Audio -from telegram._files.contact import Contact from telegram._files.document import Document -from telegram._files.location import Location from telegram._files.photosize import PhotoSize from telegram._files.sticker import Sticker from telegram._files.venue import Venue @@ -45,12 +43,13 @@ from telegram._poll import Poll from telegram._story import Story from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput if TYPE_CHECKING: - from telegram import Bot + from telegram._files.contact import Contact + from telegram._files.location import Location class ExternalReplyInfo(TelegramObject): diff --git a/src/telegram/_shared.py b/src/telegram/_shared.py index 069aeb47a6e..66c095b11aa 100644 --- a/src/telegram/_shared.py +++ b/src/telegram/_shared.py @@ -19,17 +19,13 @@ """This module contains two objects used for request chats/users service messages.""" from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject -from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict from telegram._utils.usernames import get_full_name, get_link, get_name -if TYPE_CHECKING: - from telegram._bot import Bot - class UsersShared(TelegramObject): """ diff --git a/src/telegram/_story.py b/src/telegram/_story.py index de6fbff5882..85d17f8935d 100644 --- a/src/telegram/_story.py +++ b/src/telegram/_story.py @@ -18,16 +18,11 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object related to a Telegram Story.""" -from typing import TYPE_CHECKING - from telegram._chat import Chat from telegram._telegramobject import TelegramObject from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput, TimePeriod -if TYPE_CHECKING: - from telegram import Bot - class Story(TelegramObject): """ diff --git a/src/telegram/_suggestedpost.py b/src/telegram/_suggestedpost.py index 5a49c75cd71..19e66bf92eb 100644 --- a/src/telegram/_suggestedpost.py +++ b/src/telegram/_suggestedpost.py @@ -19,7 +19,7 @@ """This module contains objects related to Telegram suggested posts.""" import datetime as dtm -from typing import TYPE_CHECKING, Final, Optional +from typing import Final from telegram import constants from telegram._message import Message @@ -28,9 +28,6 @@ from telegram._utils import enum from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class SuggestedPostPrice(TelegramObject): """ diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index 4b41e7012f3..073d5dd5626 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -30,7 +30,6 @@ from itertools import chain from types import MappingProxyType from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast, get_args, get_origin -from collections.abc import Callable from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp, to_timestamp from telegram._utils.defaultvalue import DefaultValue @@ -38,6 +37,8 @@ from telegram._utils.warnings import warn if TYPE_CHECKING: + from collections.abc import Callable + from telegram import Bot Tele_co = TypeVar("Tele_co", bound="TelegramObject", covariant=True) @@ -53,7 +54,7 @@ def _telegram_ns() -> dict[str, object]: def _unwrap_optional(ann: object) -> object: """``X | None`` → ``X``. Any other annotation is returned unchanged.""" if isinstance(ann, _types.UnionType): - non_none = [a for a in ann.__args__ if a is not type(None)] + non_none = [a for a in ann.__args__ if a is not type(None)] # pylint: disable=unidiomatic-typecheck if len(non_none) == 1: return non_none[0] return ann @@ -78,7 +79,7 @@ def _make_seq_transform( # inspect.signature does not resolve forward refs in Sequence's despite eval_str=True for some # reason: if isinstance(item_type, str): - item_type = eval(item_type, globalns, tg_ns) # noqa: S307 + item_type = eval(item_type, globalns, tg_ns) # pylint: disable=eval-used # noqa: S307 item_origin = get_origin(item_type) if item_origin is Sequence: @@ -88,10 +89,10 @@ def _make_seq_transform( inner_fn = _make_seq_transform(inner_args[0], globalns, tg_ns) if inner_fn is None: return None - return lambda v, b, _f=inner_fn: [_f(row, b) for row in v] if isinstance(v, list) else v + return lambda v, b, _f=inner_fn: [_f(row, b) for row in v] if isinstance(v, list) else v # type: ignore[misc] # pylint: disable=line-too-long if isinstance(item_type, type) and issubclass(item_type, TelegramObject): - return lambda v, b, _c=item_type: _c.de_list(v, b) if isinstance(v, list) else v + return lambda v, b, _c=item_type: _c.de_list(v, b) if isinstance(v, list) else v # type: ignore[misc] # pylint: disable=line-too-long return None @@ -483,11 +484,11 @@ def _build_plan(cls) -> dict[str, Any]: init_fn = cls.__dict__.get("__init__") if init_fn is None: # No own __init__: inherit the nearest ancestor's plan. This is true for e.g. Chat - parent = cls.__mro__[1] + parent = cast("type[TelegramObject]", cls.__mro__[1]) if "__DE_JSON_PLAN__" not in parent.__dict__: - parent._build_plan() - cls.__DE_JSON_PLAN__ = parent.__DE_JSON_PLAN__ - cls.__DE_JSON_COMPAT__ = parent.__DE_JSON_COMPAT__ + parent._build_plan() # pylint: disable=protected-access, no-member + cls.__DE_JSON_PLAN__ = parent.__DE_JSON_PLAN__ # pylint: disable=no-member + cls.__DE_JSON_COMPAT__ = parent.__DE_JSON_COMPAT__ # pylint: disable=no-member return cls.__DE_JSON_PLAN__ plan: dict[str, Any] = {} @@ -529,7 +530,7 @@ def _build_plan(cls) -> dict[str, Any]: _, dispatch_mapping = cls.__DE_JSON_DISPATCH__ for key, value in dispatch_mapping.items(): if isinstance(value, str): - dispatch_mapping[key] = tg_ns[value] + dispatch_mapping[key] = tg_ns[value] # type: ignore[assignment] return plan @@ -602,7 +603,7 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel # Dispatch to subclass for delegator classes (e.g. TransactionPartner, ChatMember). if cls.__DE_JSON_DISPATCH__: dispatch_key, dispatch_mapping = cls.__DE_JSON_DISPATCH__ - target_cls: Tele_co = dispatch_mapping.get(data.get(dispatch_key)) + target_cls: Tele_co = dispatch_mapping.get(data.get(dispatch_key)) # type: ignore[assignment, arg-type] # pylint: disable=line-too-long if target_cls is not None: data.pop(dispatch_key) return target_cls.de_json(data=data, bot=bot) @@ -630,7 +631,7 @@ def de_json(cls: type[Tele_co], data: JSONDict, bot: "Bot | None" = None) -> Tel data[key] = from_timestamp(data[key], tzinfo=tz) elif isinstance(target, type): # Target is a TelegramObject subclass → de_json if not isinstance(data[key], target): # Avoid retransformations - data[key] = target.de_json(data[key], bot) + data[key] = target.de_json(data[key], bot) # type: ignore[attr-defined] else: # Sequence transform callable (e.g. de_list) data[key] = target(data[key], bot) diff --git a/src/telegram/_uniquegift.py b/src/telegram/_uniquegift.py index f0a32b36c31..56a3a7ed90f 100644 --- a/src/telegram/_uniquegift.py +++ b/src/telegram/_uniquegift.py @@ -21,19 +21,16 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING, Final +from typing import Final from telegram import constants from telegram._chat import Chat from telegram._files.sticker import Sticker from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional, parse_sequence_arg +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class UniqueGiftColors(TelegramObject): """This object contains information about the color scheme for a user's name, message replies diff --git a/src/telegram/_update.py b/src/telegram/_update.py index fe86792c646..335c081d4af 100644 --- a/src/telegram/_update.py +++ b/src/telegram/_update.py @@ -39,7 +39,7 @@ from telegram._utils.warnings import warn if TYPE_CHECKING: - from telegram import Bot, Chat, User + from telegram import Chat, User class Update(TelegramObject): diff --git a/src/telegram/_userprofileaudios.py b/src/telegram/_userprofileaudios.py index b1b924a3033..dfe1362fb4e 100644 --- a/src/telegram/_userprofileaudios.py +++ b/src/telegram/_userprofileaudios.py @@ -19,15 +19,11 @@ """This module contains an object that represents a Telegram UserProfileAudios.""" from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._files.audio import Audio from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class UserProfileAudios(TelegramObject): """ diff --git a/src/telegram/_userprofilephotos.py b/src/telegram/_userprofilephotos.py index 036378f4efa..e5518a2081a 100644 --- a/src/telegram/_userprofilephotos.py +++ b/src/telegram/_userprofilephotos.py @@ -19,15 +19,11 @@ """This module contains an object that represents a Telegram UserProfilePhotos.""" from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class UserProfilePhotos(TelegramObject): """This object represents a user's profile pictures. diff --git a/src/telegram/_videochat.py b/src/telegram/_videochat.py index c2d4c8c18b5..c355fe607b9 100644 --- a/src/telegram/_videochat.py +++ b/src/telegram/_videochat.py @@ -20,7 +20,6 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._telegramobject import TelegramObject from telegram._user import User @@ -28,9 +27,6 @@ from telegram._utils.datetime import get_timedelta_value from telegram._utils.types import JSONDict, TimePeriod -if TYPE_CHECKING: - from telegram import Bot - class VideoChatStarted(TelegramObject): """ diff --git a/src/telegram/_webhookinfo.py b/src/telegram/_webhookinfo.py index 9237f868371..7bfd6413c08 100644 --- a/src/telegram/_webhookinfo.py +++ b/src/telegram/_webhookinfo.py @@ -20,15 +20,11 @@ import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict -if TYPE_CHECKING: - from telegram import Bot - class WebhookInfo(TelegramObject): """This object represents a Telegram WebhookInfo. From 40e9bdf94f9f436ef05758c1c0c8288a8b42b413 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:28:30 -0700 Subject: [PATCH 10/11] Update changelog --- changes/unreleased/5186.VngJMeztfXKDXAhcqTxJjK.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/unreleased/5186.VngJMeztfXKDXAhcqTxJjK.toml b/changes/unreleased/5186.VngJMeztfXKDXAhcqTxJjK.toml index dcb5de58488..43d894d3860 100644 --- a/changes/unreleased/5186.VngJMeztfXKDXAhcqTxJjK.toml +++ b/changes/unreleased/5186.VngJMeztfXKDXAhcqTxJjK.toml @@ -1,4 +1,4 @@ -other = "PoC: Start centralizing `de_json`" +other = "Centralize `de_json` into `TelegramObject`" [[pull_requests]] uid = "5186" author_uids = ["harshil21"] From b5cfa4f902a8ddf8a22b3af392a8a1706e70fb28 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 31 Mar 2026 04:20:37 -0700 Subject: [PATCH 11/11] Remove redundant _de_json in Message --- src/telegram/_message.py | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/src/telegram/_message.py b/src/telegram/_message.py index becf5b56bf0..3b82ca8dec0 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -73,8 +73,8 @@ from telegram._telegramobject import TelegramObject from telegram._uniquegift import UniqueGiftInfo from telegram._user import User -from telegram._utils.argumentparsing import de_json_optional, parse_sequence_arg -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp, to_timestamp +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.datetime import to_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram._utils.entities import parse_message_entities, parse_message_entity from telegram._utils.strings import TextEncoding @@ -210,30 +210,6 @@ def de_json( return Message.de_json(data=data, bot=bot) return super().de_json(data=data, bot=bot) - @classmethod - def _de_json( - cls, - data: JSONDict, - bot: "Bot | None" = None, - api_kwargs: JSONDict | None = None, - ) -> "MaybeInaccessibleMessage": - """See :meth:`telegram.TelegramObject.de_json`.""" - # Idempotent: the plan may have already converted these fields. - date_val = data.get("date") - if date_val is not None and not isinstance(date_val, dtm.datetime): - # print("this was an int, converting to datetime in MaybeInaccessibleMessage") - if date_val == 0: - data["date"] = ZERO_DATE - else: - loc_tzinfo = extract_tzinfo_from_defaults(bot) - data["date"] = from_timestamp(date_val, tzinfo=loc_tzinfo) - - if not isinstance(data.get("chat"), Chat): - # print("De-jsoning chat in MaybeInaccessibleMessage") - data["chat"] = de_json_optional(data.get("chat"), Chat, bot) - - return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) - class InaccessibleMessage(MaybeInaccessibleMessage): """This object represents an inaccessible message.