diff --git a/docs/source/telegram.telegramobject.rst b/docs/source/telegram.telegramobject.rst index 9a3d85d6c97..c9ce365a461 100644 --- a/docs/source/telegram.telegramobject.rst +++ b/docs/source/telegram.telegramobject.rst @@ -4,4 +4,4 @@ telegram.TelegramObject .. autoclass:: telegram.TelegramObject :members: :show-inheritance: - :special-members: __repr__ + :special-members: __repr__, __getitem__, __eq__, __hash__, __setstate__, __getstate__, __deepcopy__ diff --git a/telegram/_telegramobject.py b/telegram/_telegramobject.py index a9e5834925a..c1cff67a87e 100644 --- a/telegram/_telegramobject.py +++ b/telegram/_telegramobject.py @@ -35,20 +35,12 @@ class TelegramObject: """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. - - When objects of this type are pickled, the :class:`~telegram.Bot` attribute associated with the - object will be removed. However, when copying the object via :func:`copy.deepcopy`, the copy - will have the *same* bot instance associated with it, i.e:: - - assert telegram_object.get_bot() is copy.deepcopy(telegram_object).get_bot() + Objects of this type are subscriptable with strings. See :meth:`__getitem__` for more details. + The :mod:`pickle` and :func:`~copy.deepcopy` behavior of objects of this type are defined by + :meth:`__getstate__`, :meth:`__setstate__` and :meth:`__deepcopy__`. .. versionchanged:: 20.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. * 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. @@ -106,7 +98,7 @@ def _apply_api_kwargs(self) -> None: 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. + have the value :obj:`None` or are 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 @@ -139,6 +131,30 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({contents})" def __getitem__(self, item: str) -> object: + """ + Objects of this type are subscriptable with strings, where + ``telegram_object["attribute_name"]`` is equivalent to ``telegram_object.attribute_name``. + + Tip: + This is useful for dynamic attribute lookup, i.e. ``telegram_object[arg]`` where the + value of ``arg`` is determined at runtime. + In all other cases, it's recommended to use the dot notation instead, i.e. + ``telegram_object.attribute_name``. + + .. versionchanged:: 20.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. + + Args: + item (:obj:`str`): The name of the attribute to look up. + + Returns: + :obj:`object` + + Raises: + :exc:`KeyError`: If the object does not have an attribute with the appropriate name. + """ if item == "from": item = "from_user" try: @@ -151,15 +167,28 @@ def __getitem__(self, item: str) -> object: def __getstate__(self) -> Dict[str, Union[str, object]]: """ - This method is used for pickling. We remove the bot attribute of the object since those - are not pickable. + Overrides :meth:`object.__getstate__` to customize the pickling process of objects of this + type. + The returned state does `not` contain the :class:`telegram.Bot` instance set with + :meth:`set_bot` (if any), as it can't be pickled. + + Returns: + state (Dict[:obj:`str`, :obj:`object`]): The state of the object. """ return self._get_attrs(include_private=True, recursive=False, remove_bot=True) def __setstate__(self, state: dict) -> None: """ - This method is used for unpickling. The data, which is in the form a dictionary, is - converted back into a class. Should be modified in place. + Overrides :meth:`object.__setstate__` to customize the unpickling process of objects of + this type. Modifies the object in-place. + If any data was stored in the :attr:`api_kwargs` of the pickled object, this method checks + if the class now has dedicated attributes for those keys and moves the values from + :attr:`api_kwargs` to the dedicated attributes. + This can happen, if serialized data is loaded with a new version of this library, where + the new version was updated to account for updates of the Telegram Bot API. + + Args: + state (:obj:`dict`): The data to set as attributes of this object. """ # Make sure that we have a `_bot` attribute. This is necessary, since __getstate__ omits # this as Bots are not pickable. @@ -170,7 +199,20 @@ def __setstate__(self, state: dict) -> None: self._apply_api_kwargs() def __deepcopy__(self: Tele_co, memodict: dict) -> Tele_co: - """This method deepcopies the object and sets the bot on the newly created copy.""" + """ + Customizes how :func:`copy.deepcopy` processes objects of this type. + The only difference to the default implementation is that the :class:`telegram.Bot` + instance set via :meth:`set_bot` (if any) is not copied, but shared between the original + and the copy, i.e.:: + + assert telegram_object.get_bot() is copy.deepcopy(telegram_object).get_bot() + + Args: + memodict (:obj:`dict`): A dictionary that maps objects to their copies. + + Returns: + :obj:`telegram.TelegramObject`: The copied object. + """ bot = self._bot # Save bot so we can set it after copying self.set_bot(None) # set to None so it is not deepcopied cls = self.__class__ @@ -359,6 +401,26 @@ def set_bot(self, bot: Optional["Bot"]) -> None: self._bot = bot def __eq__(self, other: object) -> bool: + """Compares this object with :paramref:`other` in terms of equality. + If this object and :paramref:`object` are `not` objects of the same class, + this comparison will fall back to Pythons default implementation of :meth:`object.__eq__`. + Otherwise, both objects may be compared in terms of equality, if the corresponding + subclass of :class:`TelegramObject` has defined a set of attributes to compare and + the objects are considered to be equal, if all of these attributes are equal. + If the subclass has not defined a set of attributes to compare, a warning will be issued. + + Tip: + If instances of a class in the :mod:`telegram` module are comparable in terms of + equality, the documentation of the class will state the attributes that will be used + for this comparison. + + Args: + other (:obj:`object`): The object to compare with. + + Returns: + :obj:`bool` + + """ if isinstance(other, self.__class__): if not self._id_attrs: warn( @@ -376,6 +438,12 @@ def __eq__(self, other: object) -> bool: return super().__eq__(other) def __hash__(self) -> int: + """Builds a hash value for this object such that the hash of two objects is equal if and + only if the objects are equal in terms of :meth:`__eq__`. + + Returns: + :obj:`int` + """ if self._id_attrs: return hash((self.__class__, self._id_attrs)) return super().__hash__()