Skip to content

Remove BP.insert/replace_bot #2893

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Mar 12, 2022
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
26 changes: 18 additions & 8 deletions telegram/_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import functools
import logging
import pickle
from datetime import datetime

from typing import (
Expand All @@ -37,6 +38,7 @@
cast,
Sequence,
Any,
NoReturn,
)

try:
Expand Down Expand Up @@ -123,10 +125,13 @@ class Bot(TelegramObject):
considered equal, if their :attr:`bot` is equal.

Note:
Most bot methods have the argument ``api_kwargs`` which allows to pass arbitrary keywords
to the Telegram API. This can be used to access new features of the API before they were
incorporated into PTB. However, this is not guaranteed to work, i.e. it will fail for
passing files.
* Most bot methods have the argument ``api_kwargs`` which allows passing arbitrary keywords
to the Telegram API. This can be used to access new features of the API before they are
incorporated into PTB. However, this is not guaranteed to work, i.e. it will fail for
passing files.
* Bots should not be serialized since if you for e.g. change the bots token, then your
serialized instance will not reflect that change. Trying to pickle a bot instance will
raise :exc:`pickle.PicklingError`.

.. versionchanged:: 14.0

Expand All @@ -136,6 +141,7 @@ class Bot(TelegramObject):
* Removed the deprecated ``defaults`` parameter. If you want to use
:class:`telegram.ext.Defaults`, please use the subclass :class:`telegram.ext.ExtBot`
instead.
* Attempting to pickle a bot instance will now raise :exc:`pickle.PicklingError`.

Args:
token (:obj:`str`): Bot's unique authentication.
Expand All @@ -157,7 +163,7 @@ class Bot(TelegramObject):
'private_key',
'_bot_user',
'_request',
'logger',
'_logger',
)

def __init__(
Expand All @@ -176,7 +182,7 @@ def __init__(
self._bot_user: Optional[User] = None
self._request = request or Request()
self.private_key = None
self.logger = logging.getLogger(__name__)
self._logger = logging.getLogger(__name__)

if private_key:
if not CRYPTO_INSTALLED:
Expand All @@ -188,6 +194,10 @@ def __init__(
private_key, password=private_key_password, backend=default_backend()
)

def __reduce__(self) -> NoReturn:
"""Called by pickle.dumps(). Serializing bots is unadvisable, so we forbid pickling."""
raise pickle.PicklingError('Bot objects cannot be pickled!')

# TODO: After https://youtrack.jetbrains.com/issue/PY-50952 is fixed, we can revisit this and
# consider adding Paramspec from typing_extensions to properly fix this. Currently a workaround
def _log(func: Any): # type: ignore[no-untyped-def] # skipcq: PY-D0003
Expand Down Expand Up @@ -2999,9 +3009,9 @@ def get_updates(
)

if result:
self.logger.debug('Getting updates: %s', [u['update_id'] for u in result])
self._logger.debug('Getting updates: %s', [u['update_id'] for u in result])
else:
self.logger.debug('No new updates found.')
self._logger.debug('No new updates found.')

return Update.de_list(result, self) # type: ignore[return-value]

Expand Down
2 changes: 1 addition & 1 deletion telegram/_files/_basemedium.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class _BaseMedium(TelegramObject):

"""

__slots__ = ('bot', 'file_id', 'file_size', 'file_unique_id')
__slots__ = ('file_id', 'file_size', 'file_unique_id')

def __init__(
self, file_id: str, file_unique_id: str, file_size: int = None, bot: 'Bot' = None
Expand Down
17 changes: 8 additions & 9 deletions telegram/_passport/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,14 +210,14 @@ class Credentials(TelegramObject):
nonce (:obj:`str`): Bot-specified nonce
"""

__slots__ = ('bot', 'nonce', 'secure_data')
__slots__ = ('nonce', 'secure_data')

def __init__(self, secure_data: 'SecureData', nonce: str, bot: 'Bot' = None, **_kwargs: Any):
# Required
self.secure_data = secure_data
self.nonce = nonce

self.bot = bot
self.set_bot(bot)

@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Credentials']:
Expand Down Expand Up @@ -261,7 +261,6 @@ class SecureData(TelegramObject):
"""

__slots__ = (
'bot',
'utility_bill',
'personal_details',
'temporary_registration',
Expand Down Expand Up @@ -304,7 +303,7 @@ def __init__(
self.passport = passport
self.personal_details = personal_details

self.bot = bot
self.set_bot(bot)

@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SecureData']:
Expand Down Expand Up @@ -360,7 +359,7 @@ class SecureValue(TelegramObject):

"""

__slots__ = ('data', 'front_side', 'reverse_side', 'selfie', 'files', 'translation', 'bot')
__slots__ = ('data', 'front_side', 'reverse_side', 'selfie', 'files', 'translation')

def __init__(
self,
Expand All @@ -380,7 +379,7 @@ def __init__(
self.files = files
self.translation = translation

self.bot = bot
self.set_bot(bot)

@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SecureValue']:
Expand Down Expand Up @@ -412,17 +411,17 @@ def to_dict(self) -> JSONDict:
class _CredentialsBase(TelegramObject):
"""Base class for DataCredentials and FileCredentials."""

__slots__ = ('hash', 'secret', 'file_hash', 'data_hash', 'bot')
__slots__ = ('hash', 'secret', 'file_hash', 'data_hash')

def __init__(self, hash: str, secret: str, bot: 'Bot' = None, **_kwargs: Any):
self.hash = hash
self.secret = secret

# Aliases just be be sure
# Aliases just to be sure
self.file_hash = self.hash
self.data_hash = self.hash

self.bot = bot
self.set_bot(bot)


class DataCredentials(_CredentialsBase):
Expand Down
122 changes: 93 additions & 29 deletions telegram/_telegramobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Base class for Telegram Objects."""
from copy import deepcopy

try:
import ujson as json
except ImportError:
import json # type: ignore[no-redef]

from typing import TYPE_CHECKING, List, Optional, Type, TypeVar, Tuple
from typing import TYPE_CHECKING, List, Optional, Type, TypeVar, Tuple, Dict, Union

from telegram._utils.types import JSONDict
from telegram._utils.warnings import warn
Expand All @@ -40,6 +42,12 @@ class TelegramObject:
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()

.. 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.
Expand All @@ -53,15 +61,12 @@ class TelegramObject:
_bot: Optional['Bot']
# Adding slots reduces memory usage & allows for faster attribute access.
# Only instance variables should be added to __slots__.
__slots__ = (
'_id_attrs',
'_bot',
)
__slots__ = ('_id_attrs', '_bot')

# pylint: disable=unused-argument
def __new__(cls, *args: object, **kwargs: object) -> 'TelegramObject':
# We add _id_attrs in __new__ instead of __init__ since we want to add this to the slots
# w/o calling __init__ in all of the subclasses. This is what we also do in BaseFilter.
# w/o calling __init__ in all of the subclasses.
instance = super().__new__(cls)
instance._id_attrs = ()
instance._bot = None
Expand All @@ -81,6 +86,86 @@ def __getitem__(self, item: str) -> object:
f"`{item}`."
) from exc

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.
"""
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.
"""
for key, val in state.items():
setattr(self, key, val)

def __deepcopy__(self: TO, memodict: dict) -> TO:
"""This method deepcopies the object and sets the bot on the newly created copy."""
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__
result = cls.__new__(cls) # create a new instance
memodict[id(self)] = result # save the id of the object in the dict

attrs = self._get_attrs(include_private=True) # get all its attributes

for k in attrs: # now we set the attributes in the deepcopied object
setattr(result, k, deepcopy(getattr(self, k), memodict))

result.set_bot(bot) # Assign the bots back
self.set_bot(bot)
return result # type: ignore[return-value]

def _get_attrs(
self,
include_private: bool = False,
recursive: bool = False,
remove_bot: bool = False,
) -> Dict[str, Union[str, object]]:
"""This method is used for obtaining the attributes of the object.

Args:
include_private (:obj:`bool`): Whether the result should include private variables.
recursive (:obj:`bool`): If :obj:`True`, will convert any TelegramObjects (if found) in
the attributes to a dictionary. Else, preserves it as an object itself.
remove_bot (:obj:`bool`): Whether the bot should be included in the result.

Returns:
:obj:`dict`: A dict where the keys are attribute names and values are their values.
"""
data = {}

if not recursive:
try:
# __dict__ has attrs from superclasses, so no need to put in the for loop below
data.update(self.__dict__)
except AttributeError:
pass
# We want to get all attributes for the class, using self.__slots__ only includes the
# attributes used by that class itself, and not its superclass(es). Hence, we get its MRO
# and then get their attributes. The `[:-1]` slice excludes the `object` class
for cls in self.__class__.__mro__[:-1]:
for key in cls.__slots__:
if not include_private and key.startswith('_'):
continue

value = getattr(self, key, None)
if value is not None:
if recursive and hasattr(value, 'to_dict'):
data[key] = value.to_dict()
else:
data[key] = value
elif not recursive:
data[key] = value

if recursive and data.get('from_user'):
data['from'] = data.pop('from_user', None)
if remove_bot:
data.pop('_bot', None)
return data

@staticmethod
def _parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]:
return None if data is None else data.copy()
Expand Down Expand Up @@ -137,27 +222,7 @@ def to_dict(self) -> JSONDict:
Returns:
:obj:`dict`
"""
data = {}

# We want to get all attributes for the class, using self.__slots__ only includes the
# attributes used by that class itself, and not its superclass(es). Hence we get its MRO
# and then get their attributes. The `[:-2]` slice excludes the `object` class & the
# TelegramObject class itself.
attrs = {attr for cls in self.__class__.__mro__[:-2] for attr in cls.__slots__}
for key in attrs:
if key == 'bot' or key.startswith('_'):
continue

value = getattr(self, key, None)
if value is not None:
if hasattr(value, 'to_dict'):
data[key] = value.to_dict()
else:
data[key] = value

if data.get('from_user'):
data['from'] = data.pop('from_user', None)
return data
return self._get_attrs(recursive=True)

def get_bot(self) -> 'Bot':
"""Returns the :class:`telegram.Bot` instance associated with this object.
Expand All @@ -171,8 +236,7 @@ def get_bot(self) -> 'Bot':
"""
if self._bot is None:
raise RuntimeError(
'This object has no bot associated with it. \
Shortcuts cannot be used.'
'This object has no bot associated with it. Shortcuts cannot be used.'
)
return self._bot

Expand Down
Loading