From 91ffcd099e747d0b5add955d6458afeb8818247f Mon Sep 17 00:00:00 2001 From: Badiboy Date: Sun, 9 Aug 2026 16:06:51 +0300 Subject: [PATCH 1/4] Make RichText Dictionaryable Make RichText Dictionaryable Conform the fact that RichText can be str or list also. --- telebot/types.py | 206 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 188 insertions(+), 18 deletions(-) diff --git a/telebot/types.py b/telebot/types.py index 1089d111b..1bab43194 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -14679,7 +14679,7 @@ def de_json(cls, json_string): return cls(**obj) -class RichText(JsonDeserializable): +class RichText(JsonDeserializable, Dictionaryable): """ This object represents a rich formatted text. Currently, it can be either a String for plain text, an Array of :class:`RichText`, or any of the following types: @@ -14721,9 +14721,12 @@ def __init__(self, type: str, **kwargs): def de_json(cls, json_string): if json_string is None: return None if isinstance(json_string, str): + # "...can be either a String for plain text..." return json_string if isinstance(json_string, list): + # "...an Array of :class:`RichText`..." return [RichText.de_json(item) for item in json_string] + # "...or any of the following types..." obj = cls.check_json(json_string) type = obj.pop('type', None) if type == 'bold': @@ -14777,7 +14780,24 @@ def de_json(cls, json_string): elif type == 'reference_link': return RichTextReferenceLink.de_json(obj) return None - + + def to_dict(self): + data = { + 'type': self.type + } + return data + + @staticmethod + def richtext_to_dict(richtext: Union[str, List[RichText], RichText]): + if isinstance(richtext, str): + return richtext + elif isinstance(richtext, list): + return [RichText.richtext_to_dict(item) for item in richtext] + elif isinstance(richtext, RichText): + return richtext.to_dict() + return None + + class RichTextBold(RichText): """ A bold text. @@ -14804,6 +14824,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + return data + + class RichTextItalic(RichText): """ An italicized text. @@ -14830,6 +14856,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + return data + + class RichTextUnderline(RichText): """ An underlined text. @@ -14856,6 +14888,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + return data + + class RichTextStrikethrough(RichText): """ A strikethrough text. @@ -14882,6 +14920,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + return data + + class RichTextSpoiler(RichText): """ A text covered by a spoiler. @@ -14908,6 +14952,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + return data + + class RichTextDateTime(RichText): """ Formatted date and time. @@ -14942,6 +14992,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + return data + + class RichTextTextMention(RichText): """ A mention of a Telegram user by their identifier. @@ -14973,6 +15029,12 @@ def de_json(cls, json_string): obj['user'] = User.de_json(obj['user']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['user'] = self.user.to_dict() + return data + class RichTextSubscript(RichText): """ @@ -15000,6 +15062,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + return data + + class RichTextSuperscript(RichText): """ A superscript text. @@ -15026,6 +15094,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + return data + + class RichTextMarked(RichText): """ A marked text. @@ -15052,6 +15126,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + return data + + class RichTextCode(RichText): """ A monowidth text. @@ -15078,6 +15158,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + return data + + class RichTextCustomEmoji(RichText): """ A custom emoji. @@ -15107,6 +15193,13 @@ def de_json(cls, json_string): obj = cls.check_json(json_string) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['custom_emoji_id'] = self.custom_emoji_id + data['alternative_text'] = self.alternative_text + return data + + class RichTextMathematicalExpression(RichText): """ A mathematical expression. @@ -15132,6 +15225,12 @@ def de_json(cls, json_string): obj = cls.check_json(json_string) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['expression'] = self.expression + return data + + class RichTextUrl(RichText): """ A text with a link. @@ -15162,6 +15261,13 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['url'] = self.url + return data + + class RichTextEmailAddress(RichText): """ A text with an email address. @@ -15192,6 +15298,13 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['email_address'] = self.email_address + return data + + class RichTextPhoneNumber(RichText): """ A text with a phone number. @@ -15222,6 +15335,13 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['phone_number'] = self.phone_number + return data + + class RichTextBankCardNumber(RichText): """ A text with a bank card number. @@ -15252,6 +15372,13 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['bank_card_number'] = self.bank_card_number + return data + + class RichTextMention(RichText): """ A mention by a username. @@ -15270,7 +15397,6 @@ class RichTextMention(RichText): :return: Instance of the class :rtype: :class:`RichTextMention` """ - def __init__(self, text: RichText, username: str, **kwargs): super().__init__(type='mention', **kwargs) self.text: RichText = text @@ -15283,6 +15409,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['username'] = self.username + return data + class RichTextHashtag(RichText): """ @@ -15314,6 +15446,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['hashtag'] = self.hashtag + return data + class RichTextCashtag(RichText): """ @@ -15345,6 +15483,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['cashtag'] = self.cashtag + return data + class RichTextBotCommand(RichText): """ @@ -15376,6 +15520,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['bot_command'] = self.bot_command + return data + class RichTextAnchor(RichText): """ @@ -15402,6 +15552,11 @@ def de_json(cls, json_string): obj = cls.check_json(json_string) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['name'] = self.name + return data + class RichTextAnchorLink(RichText): """ @@ -15432,7 +15587,13 @@ def de_json(cls, json_string): obj = cls.check_json(json_string) obj['text'] = RichText.de_json(obj['text']) return cls(**obj) - + + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['anchor_name'] = self.anchor_name + return data + class RichTextReference(RichText): """ @@ -15452,7 +15613,6 @@ class RichTextReference(RichText): :return: Instance of the class :rtype: :class:`RichTextReference` """ - def __init__(self, text: RichText, name: str, **kwargs): super().__init__(type='reference', **kwargs) self.text: RichText = text @@ -15464,7 +15624,13 @@ def de_json(cls, json_string): obj = cls.check_json(json_string) obj['text'] = RichText.de_json(obj['text']) return cls(**obj) - + + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['name'] = self.name + return data + class RichTextReferenceLink(RichText): """ @@ -15483,9 +15649,7 @@ class RichTextReferenceLink(RichText): :return: Instance of the class :rtype: :class:`RichTextReferenceLink` - """ - def __init__(self, text: RichText, reference_name: str, **kwargs): super().__init__(type='reference_link', **kwargs) self.text: RichText = text @@ -15498,6 +15662,12 @@ def de_json(cls, json_string): obj['text'] = RichText.de_json(obj['text']) return cls(**obj) + def to_dict(self): + data = super().to_dict() + data['text'] = RichText.richtext_to_dict(self.text) + data['reference_name'] = self.reference_name + return data + class RichBlockCaption(JsonDeserializable): """ @@ -16664,7 +16834,7 @@ def __init__(self, text: RichText, **kwargs): def to_dict(self) -> dict: data = super().to_dict() - data['text'] = self.text.to_dict() + data['text'] = RichText.richtext_to_dict(self.text) return data @@ -16693,7 +16863,7 @@ def __init__(self, text: RichText, size: int, **kwargs): def to_dict(self) -> dict: data = super().to_dict() - data['text'] = self.text.to_dict() + data['text'] = RichText.richtext_to_dict(self.text) data['size'] = self.size return data @@ -16723,7 +16893,7 @@ def __init__(self, text: RichText, language: Optional[str] = None, **kwargs): def to_dict(self) -> dict: data = super().to_dict() - data['text'] = self.text.to_dict() + data['text'] = RichText.richtext_to_dict(self.text) if self.language is not None: data['language'] = self.language return data @@ -16750,7 +16920,7 @@ def __init__(self, text: RichText, **kwargs): def to_dict(self) -> dict: data = super().to_dict() - data['text'] = self.text.to_dict() + data['text'] = RichText.richtext_to_dict(self.text) return data @@ -16873,7 +17043,7 @@ def to_dict(self) -> dict: data = super().to_dict() data['blocks'] = [block.to_dict() for block in self.blocks] if self.credit is not None: - data['credit'] = self.credit.to_dict() + data['credit'] = RichText.richtext_to_dict(self.credit) return data @@ -16902,9 +17072,9 @@ def __init__(self, text: RichText, credit: Optional[RichText] = None, **kwargs): def to_dict(self) -> dict: data = super().to_dict() - data['text'] = self.text.to_dict() + data['text'] = RichText.richtext_to_dict(self.text) if self.credit is not None: - data['credit'] = self.credit.to_dict() + data['credit'] = RichText.richtext_to_dict(self.credit) return data @@ -17016,7 +17186,7 @@ def to_dict(self) -> dict: if self.is_striped is not None: data['is_striped'] = self.is_striped if self.caption is not None: - data['caption'] = self.caption.to_dict() + data['caption'] = RichText.richtext_to_dict(self.caption) return data @@ -17055,7 +17225,7 @@ def __init__( def to_dict(self) -> dict: data = super().to_dict() - data['summary'] = self.summary.to_dict() + data['summary'] = RichText.richtext_to_dict(self.summary) data['blocks'] = [block.to_dict() for block in self.blocks] if self.is_open is not None: data['is_open'] = self.is_open @@ -17292,7 +17462,7 @@ def __init__(self, text: RichText, **kwargs): def to_dict(self) -> dict: data = super().to_dict() - data['text'] = self.text.to_dict() + data['text'] = RichText.richtext_to_dict(self.text) return data From 14af3b1213e9b991e2fa51f740a982b53ef45594 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:11:14 +0000 Subject: [PATCH 2/4] Initial plan From d89ba0c27237fa76ead20e48c641b53069a6c042 Mon Sep 17 00:00:00 2001 From: Badiboy Date: Sun, 9 Aug 2026 16:11:27 +0300 Subject: [PATCH 3/4] Fix for RichTextDateTime --- telebot/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/telebot/types.py b/telebot/types.py index 1bab43194..910c253b6 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -14995,6 +14995,8 @@ def de_json(cls, json_string): def to_dict(self): data = super().to_dict() data['text'] = RichText.richtext_to_dict(self.text) + data['unix_time'] = self.unix_time + data['date_time_format'] = self.date_time_format return data From 479ac438924c509fb6eabec784c0d5dc66cac152 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:16:26 +0000 Subject: [PATCH 4/4] Add regression tests for RichText serialization shapes (plain string, nested list, nested RichText, RichTextDateTime) Co-authored-by: Badiboy <5613421+Badiboy@users.noreply.github.com> --- tests/test_types.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/test_types.py b/tests/test_types.py index ba6211af7..398100e83 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -387,3 +387,46 @@ def test_chat_administrator_rights_can_manage_tags(): can_manage_tags=True, ) assert rights.to_dict()['can_manage_tags'] is True + + +def test_rich_text_plain_string_serialization(): + # RichText as a plain string serialized through InputRichMessage/InputRichBlock + block = types.InputRichBlockParagraph(text='plain text') + msg = types.InputRichMessage(blocks=[block]) + d = msg.to_dict() + assert d['blocks'][0]['type'] == 'paragraph' + assert d['blocks'][0]['text'] == 'plain text' + + +def test_rich_text_nested_list_serialization(): + # RichText as a nested list serialized through InputRichMessage/InputRichBlock + block = types.InputRichBlockParagraph(text=['hello ', types.RichTextBold(text='world')]) + msg = types.InputRichMessage(blocks=[block]) + d = msg.to_dict() + assert d['blocks'][0]['type'] == 'paragraph' + text_list = d['blocks'][0]['text'] + assert isinstance(text_list, list) + assert text_list[0] == 'hello ' + assert text_list[1] == {'type': 'bold', 'text': 'world'} + + +def test_rich_text_nested_richtext_serialization(): + # RichText as a nested RichText object serialized through InputRichMessage/InputRichBlock + bold = types.RichTextBold(text='bold text') + block = types.InputRichBlockParagraph(text=bold) + msg = types.InputRichMessage(blocks=[block]) + d = msg.to_dict() + assert d['blocks'][0]['type'] == 'paragraph' + assert d['blocks'][0]['text'] == {'type': 'bold', 'text': 'bold text'} + + +def test_rich_text_datetime_serialization(): + # RichTextDateTime must include all required fields (unix_time, date_time_format) + dt = types.RichTextDateTime(text='Jan 1', unix_time=1740000000, date_time_format='short') + block = types.InputRichBlockParagraph(text=dt) + msg = types.InputRichMessage(blocks=[block]) + d = msg.to_dict() + text_dict = d['blocks'][0]['text'] + assert text_dict['type'] == 'date_time' + assert text_dict['unix_time'] == 1740000000 + assert text_dict['date_time_format'] == 'short'