From f68663af7e6d3df8ffe9a4d3fe8759b8a2827dd5 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 30 Oct 2022 11:21:19 +0100 Subject: [PATCH] Overhaul String Representation of `TelegramObject` (#3234) --- docs/source/telegram.telegramobject.rst | 1 + telegram/_telegramobject.py | 41 ++++++++++++++++++++-- tests/test_telegramobject.py | 46 ++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/docs/source/telegram.telegramobject.rst b/docs/source/telegram.telegramobject.rst index 61432be18..9a3d85d6c 100644 --- a/docs/source/telegram.telegramobject.rst +++ b/docs/source/telegram.telegramobject.rst @@ -4,3 +4,4 @@ telegram.TelegramObject .. autoclass:: telegram.TelegramObject :members: :show-inheritance: + :special-members: __repr__ diff --git a/telegram/_telegramobject.py b/telegram/_telegramobject.py index a0bb9ebe1..a9e583492 100644 --- a/telegram/_telegramobject.py +++ b/telegram/_telegramobject.py @@ -21,7 +21,7 @@ import inspect import json from copy import deepcopy from itertools import chain -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Sized, Tuple, Type, TypeVar, Union from telegram._utils.types import JSONDict from telegram._utils.warnings import warn @@ -52,6 +52,9 @@ class TelegramObject: * Removed argument and attribute ``bot`` for several subclasses. Use :meth:`set_bot` and :meth:`get_bot` instead. * Removed the possibility to pass arbitrary keyword arguments for several subclasses. + * String representations objects of this type was overhauled. See :meth:`__repr__` for + details. As this class doesn't implement :meth:`object.__str__`, the default + implementation will be used, which is equivalent to :meth:`__repr__`. Arguments: api_kwargs (Dict[:obj:`str`, any], optional): |toapikwargsarg| @@ -100,8 +103,40 @@ class TelegramObject: if getattr(self, key, True) is None: setattr(self, key, self.api_kwargs.pop(key)) - def __str__(self) -> str: - return str(self.to_dict()) + def __repr__(self) -> str: + """Gives a string representation of this object in the form + ``ClassName(attr_1=value_1, attr_2=value_2, ...)``, where attributes are omitted if they + have the value :obj:`None` or empty instances of :class:`collections.abc.Sized` (e.g. + :class:`list`, :class:`dict`, :class:`set`, :class:`str`, etc.). + + As this class doesn't implement :meth:`object.__str__`, the default implementation + will be used, which is equivalent to :meth:`__repr__`. + + Returns: + :obj:`str` + """ + # * `__repr__` goal is to be unambiguous + # * `__str__` goal is to be readable + # * `str()` calls `__repr__`, if `__str__` is not defined + # In our case "unambiguous" and "readable" largely coincide, so we can use the same logic. + as_dict = self._get_attrs(recursive=False, include_private=False) + + if not self.api_kwargs: + # Drop api_kwargs from the representation, if empty + as_dict.pop("api_kwargs", None) + + contents = ", ".join( + f"{k}={as_dict[k]!r}" + for k in sorted(as_dict.keys()) + if ( + as_dict[k] is not None + and not ( + isinstance(as_dict[k], Sized) + and len(as_dict[k]) == 0 # type: ignore[arg-type] + ) + ) + ) + return f"{self.__class__.__name__}({contents})" def __getitem__(self, item: str) -> object: if item == "from": diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index 5de88e6d3..e636a0068 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -24,7 +24,7 @@ from pathlib import Path import pytest -from telegram import Bot, Chat, Message, PhotoSize, TelegramObject, User +from telegram import Bot, BotCommand, Chat, Message, PhotoSize, TelegramObject, User def all_subclasses(cls): @@ -284,3 +284,47 @@ class TestTelegramObject: assert d._private == s._private # Can't test for identity since two equal strings is True assert d._bot == s._bot and d._bot is s._bot assert d.normal == s.normal + + def test_string_representation(self): + class TGO(TelegramObject): + def __init__(self, api_kwargs=None): + super().__init__(api_kwargs=api_kwargs) + self.string_attr = "string" + self.int_attr = 42 + self.to_attr = BotCommand("command", "description") + self.list_attr = [ + BotCommand("command_1", "description_1"), + BotCommand("command_2", "description_2"), + ] + self.dict_attr = { + BotCommand("command_1", "description_1"): BotCommand( + "command_2", "description_2" + ) + } + self.empty_tuple_attrs = () + self.empty_str_attribute = "" + # Should not be included in string representation + self.none_attr = None + + expected_without_api_kwargs = ( + "TGO(dict_attr={BotCommand(command='command_1', description='description_1'): " + "BotCommand(command='command_2', description='description_2')}, int_attr=42, " + "list_attr=[BotCommand(command='command_1', description='description_1'), " + "BotCommand(command='command_2', description='description_2')], " + "string_attr='string', to_attr=BotCommand(command='command', " + "description='description'))" + ) + assert str(TGO()) == expected_without_api_kwargs + assert repr(TGO()) == expected_without_api_kwargs + + expected_with_api_kwargs = ( + "TGO(api_kwargs={'foo': 'bar'}, dict_attr={BotCommand(command='command_1', " + "description='description_1'): BotCommand(command='command_2', " + "description='description_2')}, int_attr=42, " + "list_attr=[BotCommand(command='command_1', description='description_1'), " + "BotCommand(command='command_2', description='description_2')], " + "string_attr='string', to_attr=BotCommand(command='command', " + "description='description'))" + ) + assert str(TGO(api_kwargs={"foo": "bar"})) == expected_with_api_kwargs + assert repr(TGO(api_kwargs={"foo": "bar"})) == expected_with_api_kwargs