Skip to content
5 changes: 1 addition & 4 deletions telegram/_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,16 +646,13 @@ def effective_attachment(

for attachment_type in MessageAttachmentType:
if self[attachment_type]:
self._effective_attachment = self[attachment_type]
self._effective_attachment = self[attachment_type] # type: ignore[assignment]
break
else:
self._effective_attachment = None

return self._effective_attachment # type: ignore[return-value]

def __getitem__(self, item: str) -> Any: # pylint: disable=inconsistent-return-statements
return self.chat.id if item == 'chat_id' else super().__getitem__(item)

def to_dict(self) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict()
Expand Down
21 changes: 19 additions & 2 deletions telegram/_telegramobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,16 @@


class TelegramObject:
"""Base class for most Telegram objects."""
"""Base class for most Telegram objects.

Objects of this type are subscriptable with strings, where ``telegram_object[attribute_name]``
is equivalent to ``telegram_object.attribute_name``. If the object does not have an attribute
with the appropriate name, a :exc:`KeyError` will be raised.

.. versionchanged:: 14.0
``telegram_object['from']`` will look up the key ``from_user``. This is to account for
special cases like :attr:`Message.from_user` that deviate from the official Bot API.
"""

# type hints in __new__ are not read by mypy (https://github.com/python/mypy/issues/1021). As a
# workaround we can type hint instance variables in __new__ using a syntax defined in PEP 526 -
Expand Down Expand Up @@ -62,7 +71,15 @@ def __str__(self) -> str:
return str(self.to_dict())

def __getitem__(self, item: str) -> object:
return getattr(self, item, None)
if item == 'from':
item = 'from_user'
try:
return getattr(self, item)
except AttributeError as exc:
raise KeyError(
f"Objects of type {self.__class__.__name__} don't have an attribute called "
f"`{item}`."
) from exc

@staticmethod
def _parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]:
Expand Down
5 changes: 0 additions & 5 deletions tests/test_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,6 @@ def test_all_possibilities_de_json_and_to_dict(self, bot, message_params):

assert new.to_dict() == message_params.to_dict()

def test_dict_approach(self, message):
assert message['text'] == message.text
assert message['chat_id'] == message.chat_id
assert message['no_key'] is None

def test_parse_entity(self):
text = (
b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467'
Expand Down
16 changes: 15 additions & 1 deletion tests/test_telegramobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
except ImportError:
ujson = None

from telegram import TelegramObject
from telegram import TelegramObject, Message, Chat, User


class TestTelegramObject:
Expand Down Expand Up @@ -131,3 +131,17 @@ def test_bot_instance_states(self, bot_inst):
elif bot_inst is None:
with pytest.raises(RuntimeError):
tg_object.get_bot()

def test_subscription(self):
# We test with Message because that gives us everything we want to test - easier than
# implementing a custom subclass just for this test
chat = Chat(2, Chat.PRIVATE)
user = User(3, 'first_name', False)
message = Message(1, None, chat=chat, from_user=user, text='foobar')
assert message['text'] == 'foobar'
assert message['chat'] is chat
assert message['chat_id'] == 2
assert message['from'] is user
assert message['from_user'] is user
with pytest.raises(KeyError, match="Message don't have an attribute called `no_key`"):
message['no_key']