Overhaul String Representation of TelegramObject (#3234)

This commit is contained in:
Bibo-Joshi 2022-10-30 11:21:19 +01:00 committed by GitHub
parent 07f8dd1cb1
commit f68663af7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 84 additions and 4 deletions

View file

@ -4,3 +4,4 @@ telegram.TelegramObject
.. autoclass:: telegram.TelegramObject .. autoclass:: telegram.TelegramObject
:members: :members:
:show-inheritance: :show-inheritance:
:special-members: __repr__

View file

@ -21,7 +21,7 @@ import inspect
import json import json
from copy import deepcopy from copy import deepcopy
from itertools import chain 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.types import JSONDict
from telegram._utils.warnings import warn from telegram._utils.warnings import warn
@ -52,6 +52,9 @@ class TelegramObject:
* Removed argument and attribute ``bot`` for several subclasses. Use * Removed argument and attribute ``bot`` for several subclasses. Use
:meth:`set_bot` and :meth:`get_bot` instead. :meth:`set_bot` and :meth:`get_bot` instead.
* Removed the possibility to pass arbitrary keyword arguments for several subclasses. * 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: Arguments:
api_kwargs (Dict[:obj:`str`, any], optional): |toapikwargsarg| api_kwargs (Dict[:obj:`str`, any], optional): |toapikwargsarg|
@ -100,8 +103,40 @@ class TelegramObject:
if getattr(self, key, True) is None: if getattr(self, key, True) is None:
setattr(self, key, self.api_kwargs.pop(key)) setattr(self, key, self.api_kwargs.pop(key))
def __str__(self) -> str: def __repr__(self) -> str:
return str(self.to_dict()) """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: def __getitem__(self, item: str) -> object:
if item == "from": if item == "from":

View file

@ -24,7 +24,7 @@ from pathlib import Path
import pytest 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): 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._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._bot == s._bot and d._bot is s._bot
assert d.normal == s.normal 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