Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/telegram.telegramobject.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ telegram.TelegramObject
.. autoclass:: telegram.TelegramObject
:members:
:show-inheritance:
:special-members: __repr__
41 changes: 38 additions & 3 deletions telegram/_telegramobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
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
Expand Down Expand Up @@ -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|
Expand Down Expand Up @@ -100,8 +103,40 @@ def _apply_api_kwargs(self) -> None:
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":
Expand Down
46 changes: 45 additions & 1 deletion tests/test_telegramobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

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):
Expand Down Expand Up @@ -284,3 +284,47 @@ def test_deepcopy_subclass_telegram_obj(self, bot):
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