diff --git a/telegram/bot.py b/telegram/bot.py index b8dc82daad6..ce73a2bc9e6 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -65,6 +65,7 @@ Document, File, GameHighScore, + InputMedia, Location, MaskPosition, Message, @@ -90,8 +91,6 @@ ) from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.error import InvalidToken, TelegramError -from telegram.warnings import PTBDeprecationWarning -from telegram.utils.warnings import warn from telegram.utils.defaultvalue import DEFAULT_NONE, DefaultValue, DEFAULT_20 from telegram.utils.datetime import to_timestamp from telegram.utils.files import is_local_file, parse_file_input @@ -99,13 +98,11 @@ from telegram.utils.types import FileInput, JSONDict, ODVInput, DVInput if TYPE_CHECKING: - from telegram.ext import Defaults from telegram import ( InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo, - InputMedia, InlineQueryResult, LabeledPrice, MessageEntity, @@ -147,6 +144,9 @@ class Bot(TelegramObject): * Removed the deprecated methods ``kick_chat_member``, ``kickChatMember``, ``get_chat_members_count`` and ``getChatMembersCount``. * Removed the deprecated property ``commands``. + * Removed the deprecated ``defaults`` parameter. If you want to use + :class:`telegram.ext.Defaults`, please use the subclass :class:`telegram.ext.ExtBot` + instead. Args: token (:obj:`str`): Bot's unique authentication. @@ -156,13 +156,6 @@ class Bot(TelegramObject): :obj:`telegram.request.Request`. private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data. private_key_password (:obj:`bytes`, optional): Password for above private key. - defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to - be used if not set explicitly in the bot methods. - - .. deprecated:: 13.6 - Passing :class:`telegram.ext.Defaults` to :class:`telegram.Bot` is deprecated. If - you want to use :class:`telegram.ext.Defaults`, please use - :class:`telegram.ext.ExtBot` instead. """ @@ -171,7 +164,6 @@ class Bot(TelegramObject): 'base_url', 'base_file_url', 'private_key', - 'defaults', '_bot', '_request', 'logger', @@ -185,20 +177,9 @@ def __init__( request: 'Request' = None, private_key: bytes = None, private_key_password: bytes = None, - defaults: 'Defaults' = None, ): self.token = self._validate_token(token) - # Gather default - self.defaults = defaults - - if self.defaults: - warn( - 'Passing Defaults to telegram.Bot is deprecated. Use telegram.ext.ExtBot instead.', - PTBDeprecationWarning, - stacklevel=4, - ) - if base_url is None: base_url = 'https://api.telegram.org/bot' @@ -222,41 +203,42 @@ def __init__( private_key, password=private_key_password, backend=default_backend() ) - def _insert_defaults( + def _insert_defaults( # pylint: disable=no-self-use self, data: Dict[str, object], timeout: ODVInput[float] ) -> Optional[float]: - """ - Inserts the defaults values for optional kwargs for which tg.ext.Defaults provides - convenience functionality, i.e. the kwargs with a tg.utils.helpers.DefaultValue default - - data is edited in-place. As timeout is not passed via the kwargs, it needs to be passed - separately and gets returned. - - This can only work, if all kwargs that may have defaults are passed in data! - """ - effective_timeout = DefaultValue.get_value(timeout) - - # If we have no Defaults, we just need to replace DefaultValue instances - # with the actual value - if not self.defaults: - data.update((key, DefaultValue.get_value(value)) for key, value in data.items()) - return effective_timeout - - # if we have Defaults, we replace all DefaultValue instances with the relevant - # Defaults value. If there is none, we fall back to the default value of the bot method + """This method is here to make ext.Defaults work. Because we need to be able to tell + e.g. `send_message(chat_id, text)` from `send_message(chat_id, text, parse_mode=None)`, the + default values for `parse_mode` etc are not `None` but `DEFAULT_NONE`. While this *could* + be done in ExtBot instead of Bot, shortcuts like `Message.reply_text` need to work for both + Bot and ExtBot, so they also have the `DEFAULT_NONE` default values. + + This makes it necessary to convert `DefaultValue(obj)` to `obj` at some point between + `Message.reply_text` and the request to TG. Doing this here in a centralized manner is a + rather clean and minimally invasive solution, i.e. the link between tg and tg.ext is as + small as possible. + See also _insert_defaults_for_ilq + ExtBot overrides this method to actually insert default values. + + If in the future we come up with a better way of making `Defaults` work, we can cut this + link as well. + """ + # We + # 1) set the correct parse_mode for all InputMedia objects + # 2) replace all DefaultValue instances with the corresponding normal value. for key, val in data.items(): - if isinstance(val, DefaultValue): - data[key] = self.defaults.api_defaults.get(key, val.value) - - if isinstance(timeout, DefaultValue): - # If we get here, we use Defaults.timeout, unless that's not set, which is the - # case if isinstance(self.defaults.timeout, DefaultValue) - return ( - self.defaults.timeout - if not isinstance(self.defaults.timeout, DefaultValue) - else effective_timeout - ) - return effective_timeout + # 1) + if isinstance(val, InputMedia): + val.parse_mode = DefaultValue.get_value( # type: ignore[attr-defined] + val.parse_mode # type: ignore[attr-defined] + ) + elif key == 'media' and isinstance(val, list): + for media in val: + media.parse_mode = DefaultValue.get_value(media.parse_mode) + # 2) + else: + data[key] = DefaultValue.get_value(val) + + return DefaultValue.get_value(timeout) def _post( self, @@ -279,9 +261,16 @@ def _post( effective_timeout = self._insert_defaults(data, timeout) else: effective_timeout = cast(float, timeout) + # Drop any None values because Telegram doesn't handle them well data = {key: value for key, value in data.items() if value is not None} + # We do this here so that _insert_defaults (see above) has a chance to convert + # to the default timezone in case this is called by ExtBot + for key, value in data.items(): + if isinstance(value, datetime): + data[key] = to_timestamp(value) + return self.request.post( f'{self.base_url}/{endpoint}', data=data, timeout=effective_timeout ) @@ -300,7 +289,7 @@ def _message( if reply_to_message_id is not None: data['reply_to_message_id'] = reply_to_message_id - # We don't check if (DEFAULT_)None here, so that _put is able to insert the defaults + # We don't check if (DEFAULT_)None here, so that _post is able to insert the defaults # correctly, if necessary data['disable_notification'] = disable_notification data['allow_sending_without_reply'] = allow_sending_without_reply @@ -313,12 +302,6 @@ def _message( else: data['reply_markup'] = reply_markup - if data.get('media') and (data['media'].parse_mode == DEFAULT_NONE): - if self.defaults: - data['media'].parse_mode = DefaultValue.get_value(self.defaults.parse_mode) - else: - data['media'].parse_mode = None - result = self._post(endpoint, data, timeout=timeout, api_kwargs=api_kwargs) if result is True: @@ -1455,13 +1438,6 @@ def send_media_group( 'allow_sending_without_reply': allow_sending_without_reply, } - for med in data['media']: - if med.parse_mode == DEFAULT_NONE: - if self.defaults: - med.parse_mode = DefaultValue.get_value(self.defaults.parse_mode) - else: - med.parse_mode = None - if reply_to_message_id: data['reply_to_message_id'] = reply_to_message_id @@ -2050,6 +2026,28 @@ def _effective_inline_results( # pylint: disable=R0201 return effective_results, next_offset + @no_type_check # mypy doesn't play too well with hasattr + def _insert_defaults_for_ilq_results( # pylint: disable=R0201 + self, res: 'InlineQueryResult' + ) -> None: + """The reason why this method exists is similar to the description of _insert_defaults + The reason why we do this in rather than in _insert_defaults is because converting + DEFAULT_NONE to NONE *before* calling to_dict() makes it way easier to drop None entries + from the json data. + """ + # pylint: disable=W0212 + if hasattr(res, 'parse_mode'): + res.parse_mode = DefaultValue.get_value(res.parse_mode) + if hasattr(res, 'input_message_content') and res.input_message_content: + if hasattr(res.input_message_content, 'parse_mode'): + res.input_message_content.parse_mode = DefaultValue.get_value( + res.input_message_content.parse_mode + ) + if hasattr(res.input_message_content, 'disable_web_page_preview'): + res.input_message_content.disable_web_page_preview = DefaultValue.get_value( + res.input_message_content.disable_web_page_preview + ) + @log def answer_inline_query( self, @@ -2123,44 +2121,13 @@ def answer_inline_query( :class:`telegram.error.TelegramError` """ - - @no_type_check - def _set_defaults(res): - # pylint: disable=W0212 - if hasattr(res, 'parse_mode') and res.parse_mode == DEFAULT_NONE: - if self.defaults: - res.parse_mode = self.defaults.parse_mode - else: - res.parse_mode = None - if hasattr(res, 'input_message_content') and res.input_message_content: - if ( - hasattr(res.input_message_content, 'parse_mode') - and res.input_message_content.parse_mode == DEFAULT_NONE - ): - if self.defaults: - res.input_message_content.parse_mode = DefaultValue.get_value( - self.defaults.parse_mode - ) - else: - res.input_message_content.parse_mode = None - if ( - hasattr(res.input_message_content, 'disable_web_page_preview') - and res.input_message_content.disable_web_page_preview == DEFAULT_NONE - ): - if self.defaults: - res.input_message_content.disable_web_page_preview = ( - DefaultValue.get_value(self.defaults.disable_web_page_preview) - ) - else: - res.input_message_content.disable_web_page_preview = None - effective_results, next_offset = self._effective_inline_results( results=results, next_offset=next_offset, current_offset=current_offset ) # Apply defaults for result in effective_results: - _set_defaults(result) + self._insert_defaults_for_ilq_results(result) results_dicts = [res.to_dict() for res in effective_results] @@ -2335,10 +2302,6 @@ def ban_chat_member( data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} if until_date is not None: - if isinstance(until_date, datetime): - until_date = to_timestamp( - until_date, tzinfo=self.defaults.tzinfo if self.defaults else None - ) data['until_date'] = until_date if revoke_messages is not None: @@ -3666,10 +3629,6 @@ def restrict_chat_member( } if until_date is not None: - if isinstance(until_date, datetime): - until_date = to_timestamp( - until_date, tzinfo=self.defaults.tzinfo if self.defaults else None - ) data['until_date'] = until_date result = self._post('restrictChatMember', data, timeout=timeout, api_kwargs=api_kwargs) @@ -3938,10 +3897,6 @@ def create_chat_invite_link( } if expire_date is not None: - if isinstance(expire_date, datetime): - expire_date = to_timestamp( - expire_date, tzinfo=self.defaults.tzinfo if self.defaults else None - ) data['expire_date'] = expire_date if member_limit is not None: @@ -3993,10 +3948,6 @@ def edit_chat_invite_link( data: JSONDict = {'chat_id': chat_id, 'invite_link': invite_link} if expire_date is not None: - if isinstance(expire_date, datetime): - expire_date = to_timestamp( - expire_date, tzinfo=self.defaults.tzinfo if self.defaults else None - ) data['expire_date'] = expire_date if member_limit is not None: @@ -4818,10 +4769,6 @@ def send_poll( if open_period: data['open_period'] = open_period if close_date: - if isinstance(close_date, datetime): - close_date = to_timestamp( - close_date, tzinfo=self.defaults.tzinfo if self.defaults else None - ) data['close_date'] = close_date return self._message( # type: ignore[return-value] diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index cf60d8d6ad0..52e31aa248c 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -42,9 +42,8 @@ from telegram import Update from telegram.error import TelegramError -from telegram.ext import BasePersistence, ContextTypes +from telegram.ext import BasePersistence, ContextTypes, ExtBot from telegram.ext.handler import Handler -import telegram.ext.extbot from telegram.ext.callbackdatacache import CallbackDataCache from telegram.utils.defaultvalue import DefaultValue, DEFAULT_FALSE from telegram.utils.warnings import warn @@ -231,7 +230,7 @@ def __init__( f"bot_data must be of type {self.context_types.bot_data.__name__}" ) if self.persistence.store_data.callback_data: - self.bot = cast(telegram.ext.extbot.ExtBot, self.bot) + self.bot = cast(ExtBot, self.bot) persistent_data = self.persistence.get_callback_data() if persistent_data is not None: if not isinstance(persistent_data, tuple) and len(persistent_data) != 2: @@ -495,7 +494,11 @@ def process_update(self, update: object) -> None: handled_only_async = all(sync_modes) if handled: # Respect default settings - if all(mode is DEFAULT_FALSE for mode in sync_modes) and self.bot.defaults: + if ( + all(mode is DEFAULT_FALSE for mode in sync_modes) + and isinstance(self.bot, ExtBot) + and self.bot.defaults + ): handled_only_async = self.bot.defaults.run_async # If update was only handled by async handlers, we don't need to update here if not handled_only_async: @@ -599,7 +602,7 @@ def __update_persistence(self, update: object = None) -> None: user_ids = [] if self.persistence.store_data.callback_data: - self.bot = cast(telegram.ext.extbot.ExtBot, self.bot) + self.bot = cast(ExtBot, self.bot) try: self.persistence.update_callback_data( self.bot.callback_data_cache.persistence_data @@ -639,7 +642,10 @@ def add_error_handler( Args: callback (:obj:`callable`): The callback function for this error handler. Will be called when an error is raised. - Callback signature: ``def callback(update: Update, context: CallbackContext)`` + Callback signature: + + + ``def callback(update: Update, context: CallbackContext)`` The error that happened will be present in context.error. run_async (:obj:`bool`, optional): Whether this handlers callback should be run @@ -649,7 +655,12 @@ def add_error_handler( self.logger.debug('The callback is already registered as an error handler. Ignoring.') return - if run_async is DEFAULT_FALSE and self.bot.defaults and self.bot.defaults.run_async: + if ( + run_async is DEFAULT_FALSE + and isinstance(self.bot, ExtBot) + and self.bot.defaults + and self.bot.defaults.run_async + ): run_async = True self.error_handlers[callback] = run_async diff --git a/telegram/ext/extbot.py b/telegram/ext/extbot.py index 19824830c4d..1429bc64062 100644 --- a/telegram/ext/extbot.py +++ b/telegram/ext/extbot.py @@ -19,7 +19,20 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Bot with convenience extensions.""" from copy import copy -from typing import Union, cast, List, Callable, Optional, Tuple, TypeVar, TYPE_CHECKING, Sequence +from datetime import datetime +from typing import ( + Union, + cast, + List, + Callable, + Optional, + Tuple, + TypeVar, + TYPE_CHECKING, + Sequence, + Dict, + no_type_check, +) import telegram.bot from telegram import ( @@ -31,11 +44,13 @@ Update, Chat, CallbackQuery, + InputMedia, ) from telegram.ext.callbackdatacache import CallbackDataCache from telegram.utils.types import JSONDict, ODVInput, DVInput -from telegram.utils.defaultvalue import DEFAULT_NONE +from telegram.utils.defaultvalue import DEFAULT_NONE, DefaultValue +from telegram.utils.datetime import to_timestamp if TYPE_CHECKING: from telegram import InlineQueryResult, MessageEntity @@ -73,7 +88,7 @@ class ExtBot(telegram.bot.Bot): """ - __slots__ = ('arbitrary_callback_data', 'callback_data_cache') + __slots__ = ('arbitrary_callback_data', 'callback_data_cache', '_defaults') def __init__( self, @@ -94,8 +109,7 @@ def __init__( private_key=private_key, private_key_password=private_key_password, ) - # We don't pass this to super().__init__ to avoid the deprecation warning - self.defaults = defaults + self._defaults = defaults # set up callback_data if not isinstance(arbitrary_callback_data, bool): @@ -106,6 +120,64 @@ def __init__( self.arbitrary_callback_data = arbitrary_callback_data self.callback_data_cache: CallbackDataCache = CallbackDataCache(bot=self, maxsize=maxsize) + @property + def defaults(self) -> Optional['Defaults']: + """The :class:`telegram.ext.Defaults` used by this bot, if any.""" + # This is a property because defaults shouldn't be changed at runtime + return self._defaults + + def _insert_defaults( + self, data: Dict[str, object], timeout: ODVInput[float] + ) -> Optional[float]: + """Inserts the defaults values for optional kwargs for which tg.ext.Defaults provides + convenience functionality, i.e. the kwargs with a tg.utils.helpers.DefaultValue default + + data is edited in-place. As timeout is not passed via the kwargs, it needs to be passed + separately and gets returned. + + This can only work, if all kwargs that may have defaults are passed in data! + """ + # if we have Defaults, we + # 1) replace all DefaultValue instances with the relevant Defaults value. If there is none, + # we fall back to the default value of the bot method + # 2) convert all datetime.datetime objects to timestamps wrt the correct default timezone + # 3) set the correct parse_mode for all InputMedia objects + for key, val in data.items(): + # 1) + if isinstance(val, DefaultValue): + data[key] = ( + self.defaults.api_defaults.get(key, val.value) + if self.defaults + else DefaultValue.get_value(val) + ) + + # 2) + elif isinstance(val, datetime): + data[key] = to_timestamp( + val, tzinfo=self.defaults.tzinfo if self.defaults else None + ) + + # 3) + elif isinstance(val, InputMedia) and val.parse_mode is DEFAULT_NONE: # type: ignore + val.parse_mode = ( # type: ignore[attr-defined] + self.defaults.parse_mode if self.defaults else None + ) + elif key == 'media' and isinstance(val, list): + for media in val: + if media.parse_mode is DEFAULT_NONE: + media.parse_mode = self.defaults.parse_mode if self.defaults else None + + effective_timeout = DefaultValue.get_value(timeout) + if isinstance(timeout, DefaultValue): + # If we get here, we use Defaults.timeout, unless that's not set, which is the + # case if isinstance(self.defaults.timeout, DefaultValue) + return ( + self.defaults.timeout + if self.defaults and not isinstance(self.defaults.timeout, DefaultValue) + else effective_timeout + ) + return effective_timeout + def _replace_keyboard(self, reply_markup: Optional[ReplyMarkup]) -> Optional[ReplyMarkup]: # If the reply_markup is an inline keyboard and we allow arbitrary callback data, let the # CallbackDataCache build a new keyboard with the data replaced. Otherwise return the input @@ -233,8 +305,7 @@ def _effective_inline_results( # pylint: disable=R0201 next_offset: str = None, current_offset: str = None, ) -> Tuple[Sequence['InlineQueryResult'], Optional[str]]: - """ - This method is called by Bot.answer_inline_query to build the actual results list. + """This method is called by Bot.answer_inline_query to build the actual results list. Overriding this to call self._replace_keyboard suffices """ effective_results, next_offset = super()._effective_inline_results( @@ -260,6 +331,30 @@ def _effective_inline_results( # pylint: disable=R0201 return results, next_offset + @no_type_check # mypy doesn't play too well with hasattr + def _insert_defaults_for_ilq_results(self, res: 'InlineQueryResult') -> None: + """This method is called by Bot.answer_inline_query to replace `DefaultValue(obj)` with + `obj`. + Overriding this to call insert the actual desired default values. + """ + if hasattr(res, 'parse_mode') and res.parse_mode is DEFAULT_NONE: + res.parse_mode = self.defaults.parse_mode if self.defaults else None + if hasattr(res, 'input_message_content') and res.input_message_content: + if ( + hasattr(res.input_message_content, 'parse_mode') + and res.input_message_content.parse_mode is DEFAULT_NONE + ): + res.input_message_content.parse_mode = ( + self.defaults.parse_mode if self.defaults else None + ) + if ( + hasattr(res.input_message_content, 'disable_web_page_preview') + and res.input_message_content.disable_web_page_preview is DEFAULT_NONE + ): + res.input_message_content.disable_web_page_preview = ( + self.defaults.disable_web_page_preview if self.defaults else None + ) + def stop_poll( self, chat_id: Union[int, str], diff --git a/telegram/ext/handler.py b/telegram/ext/handler.py index 4b544b82788..7e715369e57 100644 --- a/telegram/ext/handler.py +++ b/telegram/ext/handler.py @@ -23,6 +23,7 @@ from telegram.ext.utils.promise import Promise from telegram.utils.defaultvalue import DefaultValue, DEFAULT_FALSE from telegram.ext.utils.types import CCT +from .extbot import ExtBot if TYPE_CHECKING: from telegram.ext import Dispatcher @@ -112,6 +113,7 @@ def handle_update( run_async = self.run_async if ( self.run_async is DEFAULT_FALSE + and isinstance(dispatcher.bot, ExtBot) and dispatcher.bot.defaults and dispatcher.bot.defaults.run_async ): diff --git a/telegram/ext/jobqueue.py b/telegram/ext/jobqueue.py index ac255ad355b..6e17adbd420 100644 --- a/telegram/ext/jobqueue.py +++ b/telegram/ext/jobqueue.py @@ -29,6 +29,7 @@ from telegram.ext.callbackcontext import CallbackContext from telegram.utils.types import JSONDict +from .extbot import ExtBot if TYPE_CHECKING: from telegram.ext import Dispatcher @@ -119,7 +120,7 @@ def set_dispatcher(self, dispatcher: 'Dispatcher') -> None: """ self._dispatcher = dispatcher - if dispatcher.bot.defaults: + if isinstance(dispatcher.bot, ExtBot) and dispatcher.bot.defaults: self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc) def run_once( diff --git a/telegram/message.py b/telegram/message.py index 68bc0b65fd7..7348a7c3881 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -716,8 +716,10 @@ def _quote(self, quote: Optional[bool], reply_to_message_id: Optional[int]) -> O return self.message_id else: - if self.bot.defaults: - default_quote = self.bot.defaults.quote + # Unfortunately we need some ExtBot logic here because it's hard to move shortcut + # logic into ExtBot + if hasattr(self.bot, 'defaults') and self.bot.defaults: # type: ignore[union-attr] + default_quote = self.bot.defaults.quote # type: ignore[union-attr] else: default_quote = None if (default_quote is None and self.chat.type != Chat.PRIVATE) or default_quote: diff --git a/tests/conftest.py b/tests/conftest.py index 8b63ff79e83..7adb67d13d1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,6 +45,11 @@ File, ChatPermissions, Bot, + InlineQueryResultArticle, + InputTextMessageContent, + InlineQueryResultCachedPhoto, + InputMediaPhoto, + InputMedia, ) from telegram.ext import ( Dispatcher, @@ -109,6 +114,11 @@ def bot(bot_info): return DictExtBot(bot_info['token'], private_key=PRIVATE_KEY, request=DictRequest(8)) +@pytest.fixture(scope='session') +def raw_bot(bot_info): + return DictBot(bot_info['token'], private_key=PRIVATE_KEY, request=DictRequest()) + + DEFAULT_BOTS = {} @@ -525,6 +535,58 @@ def make_assertion(**kw): return True +# mainly for check_defaults_handling below +def build_kwargs(signature: inspect.Signature, default_kwargs, dfv: Any = DEFAULT_NONE): + kws = {} + for name, param in signature.parameters.items(): + # For required params we need to pass something + if param.default is inspect.Parameter.empty: + # Some special casing + if name == 'permissions': + kws[name] = ChatPermissions() + elif name in ['prices', 'commands', 'errors']: + kws[name] = [] + elif name == 'media': + media = InputMediaPhoto('media', parse_mode=dfv) + if 'list' in str(param.annotation).lower(): + kws[name] = [media] + else: + kws[name] = media + elif name == 'results': + itmc = InputTextMessageContent( + 'text', parse_mode=dfv, disable_web_page_preview=dfv + ) + kws[name] = [ + InlineQueryResultArticle('id', 'title', input_message_content=itmc), + InlineQueryResultCachedPhoto( + 'id', 'photo_file_id', parse_mode=dfv, input_message_content=itmc + ), + ] + elif name == 'ok': + kws['ok'] = False + kws['error_message'] = 'error' + else: + kws[name] = True + # pass values for params that can have defaults only if we don't want to use the + # standard default + elif name in default_kwargs: + if dfv != DEFAULT_NONE: + kws[name] = dfv + # Some special casing for methods that have "exactly one of the optionals" type args + elif name in ['location', 'contact', 'venue', 'inline_message_id']: + kws[name] = True + elif name == 'until_date': + if dfv == 'non-None-value': + # Europe/Berlin + kws[name] = pytz.timezone('Europe/Berlin').localize( + datetime.datetime(2000, 1, 1, 0) + ) + else: + # UTC + kws[name] = datetime.datetime(2000, 1, 1, 0) + return kws + + def check_defaults_handling( method: Callable, bot: ExtBot, @@ -541,31 +603,6 @@ def check_defaults_handling( """ - def build_kwargs(signature: inspect.Signature, default_kwargs, dfv: Any = DEFAULT_NONE): - kws = {} - for name, param in signature.parameters.items(): - # For required params we need to pass something - if param.default == param.empty: - # Some special casing - if name == 'permissions': - kws[name] = ChatPermissions() - elif name in ['prices', 'media', 'results', 'commands', 'errors']: - kws[name] = [] - elif name == 'ok': - kws['ok'] = False - kws['error_message'] = 'error' - else: - kws[name] = True - # pass values for params that can have defaults only if we don't want to use the - # standard default - elif name in default_kwargs: - if dfv != DEFAULT_NONE: - kws[name] = dfv - # Some special casing for methods that have "exactly one of the optionals" type args - elif name in ['location', 'contact', 'venue', 'inline_message_id']: - kws[name] = True - return kws - shortcut_signature = inspect.signature(method) kwargs_need_default = [ kwarg @@ -575,23 +612,20 @@ def build_kwargs(signature: inspect.Signature, default_kwargs, dfv: Any = DEFAUL # shortcut_signature.parameters['timeout'] is of type DefaultValue method_timeout = shortcut_signature.parameters['timeout'].default.value - default_kwarg_names = kwargs_need_default - # special case explanation_parse_mode of Bot.send_poll: - if 'explanation_parse_mode' in default_kwarg_names: - default_kwarg_names.remove('explanation_parse_mode') - defaults_no_custom_defaults = Defaults() - defaults_custom_defaults = Defaults( - **{kwarg: 'custom_default' for kwarg in default_kwarg_names} - ) + kwargs = {kwarg: 'custom_default' for kwarg in inspect.signature(Defaults).parameters.keys()} + kwargs['tzinfo'] = pytz.timezone('America/New_York') + defaults_custom_defaults = Defaults(**kwargs) expected_return_values = [None, []] if return_value is None else [return_value] def make_assertion(_, data, timeout=DEFAULT_NONE, df_value=DEFAULT_NONE): - expected_timeout = method_timeout if df_value == DEFAULT_NONE else df_value + # Check timeout first + expected_timeout = method_timeout if df_value is DEFAULT_NONE else df_value if timeout != expected_timeout: pytest.fail(f'Got value {timeout} for "timeout", expected {expected_timeout}') + # Check regular arguments that need defaults for arg in (dkw for dkw in kwargs_need_default if dkw != 'timeout'): # 'None' should not be passed along to Telegram if df_value in [None, DEFAULT_NONE]: @@ -604,6 +638,65 @@ def make_assertion(_, data, timeout=DEFAULT_NONE, df_value=DEFAULT_NONE): if value != df_value: pytest.fail(f'Got value {value} for argument {arg} instead of {df_value}') + # Check InputMedia (parse_mode can have a default) + def check_input_media(m: InputMedia): + parse_mode = m.parse_mode + if df_value is DEFAULT_NONE: + if parse_mode is not None: + pytest.fail('InputMedia has non-None parse_mode') + elif parse_mode != df_value: + pytest.fail( + f'Got value {parse_mode} for InputMedia.parse_mode instead of {df_value}' + ) + + media = data.pop('media', None) + if media: + if isinstance(media, InputMedia): + check_input_media(media) + else: + for m in media: + check_input_media(m) + + # Check InlineQueryResults + results = data.pop('results', []) + for result in results: + if df_value in [DEFAULT_NONE, None]: + if 'parse_mode' in result: + pytest.fail('ILQR has a parse mode, expected it to be absent') + # Here we explicitly use that we only pass ILQRPhoto and ILQRArticle for testing + # so ILQRPhoto is expected to have parse_mode if df_value is not in [DF_NONE, NONE] + elif 'photo' in result and result.get('parse_mode') != df_value: + pytest.fail( + f'Got value {result.get("parse_mode")} for ' + f'ILQR.parse_mode instead of {df_value}' + ) + imc = result.get('input_message_content') + if not imc: + continue + for attr in ['parse_mode', 'disable_web_page_preview']: + if df_value in [DEFAULT_NONE, None]: + if attr in imc: + pytest.fail(f'ILQR.i_m_c has a {attr}, expected it to be absent') + # Here we explicitly use that we only pass InputTextMessageContent for testing + # which has both attributes + elif imc.get(attr) != df_value: + pytest.fail( + f'Got value {imc.get(attr)} for ILQR.i_m_c.{attr} instead of {df_value}' + ) + + # Check datetime conversion + until_date = data.pop('until_date', None) + if until_date: + if df_value == 'non-None-value': + if until_date != 946681200: + pytest.fail('Non-naive until_date was interpreted as Europe/Berlin.') + if df_value is DEFAULT_NONE: + if until_date != 946684800: + pytest.fail('Naive until_date was not interpreted as UTC') + if df_value == 'custom_default': + if until_date != 946702800: + pytest.fail('Naive until_date was not interpreted as America/New_York') + if method.__name__ in ['get_file', 'get_small_file', 'get_big_file']: # This is here mainly for PassportFile.get_file, which calls .set_credentials on the # return value @@ -623,7 +716,7 @@ def make_assertion(_, data, timeout=DEFAULT_NONE, df_value=DEFAULT_NONE): (DEFAULT_NONE, defaults_no_custom_defaults), ('custom_default', defaults_custom_defaults), ]: - bot.defaults = defaults + bot._defaults = defaults # 1: test that we get the correct default value, if we don't specify anything kwargs = build_kwargs( shortcut_signature, @@ -652,6 +745,6 @@ def make_assertion(_, data, timeout=DEFAULT_NONE, df_value=DEFAULT_NONE): raise exc finally: setattr(bot.request, 'post', orig_post) - bot.defaults = None + bot._defaults = None return True diff --git a/tests/test_bot.py b/tests/test_bot.py index 8cf62962431..824a7ef7208 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -51,14 +51,17 @@ InlineQueryResultVoice, PollOption, BotCommandScopeChat, + File, + InputMedia, ) from telegram.constants import MAX_INLINE_QUERY_RESULTS -from telegram.ext import ExtBot, Defaults +from telegram.ext import ExtBot from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter, TelegramError from telegram.ext.callbackdatacache import InvalidCallbackData from telegram.utils.datetime import from_timestamp, to_timestamp from telegram.helpers import escape_markdown -from tests.conftest import expect_bad_request, check_defaults_handling, GITHUB_ACTION +from telegram.utils.defaultvalue import DefaultValue +from tests.conftest import expect_bad_request, check_defaults_handling, GITHUB_ACTION, build_kwargs from tests.bots import FALLBACKS @@ -246,9 +249,16 @@ def test_to_dict(self, bot): ] ], ) - def test_defaults_handling(self, bot_method_name, bot): + def test_defaults_handling(self, bot_method_name, bot, raw_bot, monkeypatch): """ - Here we check that the bot methods handle tg.ext.Defaults correctly. As for most defaults, + Here we check that the bot methods handle tg.ext.Defaults correctly. This has two parts: + + 1. Check that ExtBot actually inserts the defaults values correctly + 2. Check that tg.Bot just replaces `DefaultValue(obj)` with `obj`, i.e. that it doesn't + pass any `DefaultValue` instances to Request. See the docstring of + tg.Bot._insert_defaults for details on why we need that + + As for most defaults, we can't really check the effect, we just check if we're passing the correct kwargs to Request.post. As bot method tests a scattered across the different test files, we do this here in one place. @@ -259,9 +269,61 @@ def test_defaults_handling(self, bot_method_name, bot): Finally, there are some tests for Defaults.{parse_mode, quote, allow_sending_without_reply} at the appropriate places, as those are the only things we can actually check. """ + # Check that ExtBot does the right thing bot_method = getattr(bot, bot_method_name) assert check_defaults_handling(bot_method, bot) + # check that tg.Bot does the right thing + # make_assertion basically checks everything that happens in + # Bot._insert_defaults and Bot._insert_defaults_for_ilq_results + def make_assertion(_, data, timeout=None): + # Check regular kwargs + for k, v in data.items(): + if isinstance(v, DefaultValue): + pytest.fail(f'Parameter {k} was passed as DefaultValue to request') + elif isinstance(v, InputMedia) and isinstance(v.parse_mode, DefaultValue): + pytest.fail(f'Parameter {k} has a DefaultValue parse_mode') + # Check InputMedia + elif k == 'media' and isinstance(v, list): + if any(isinstance(med.parse_mode, DefaultValue) for med in v): + pytest.fail('One of the media items has a DefaultValue parse_mode') + # Check timeout + if isinstance(timeout, DefaultValue): + pytest.fail('Parameter timeout was passed as DefaultValue to request') + # Check inline query results + if bot_method_name.lower().replace('_', '') == 'answerinlinequery': + for result_dict in data['results']: + if isinstance(result_dict.get('parse_mode'), DefaultValue): + pytest.fail('InlineQueryResult has DefaultValue parse_mode') + imc = result_dict.get('input_message_content') + if imc and isinstance(imc.get('parse_mode'), DefaultValue): + pytest.fail( + 'InlineQueryResult is InputMessageContext with DefaultValue parse_mode' + ) + if imc and isinstance(imc.get('disable_web_page_preview'), DefaultValue): + pytest.fail( + 'InlineQueryResult is InputMessageContext with DefaultValue ' + 'disable_web_page_preview ' + ) + # Check datetime conversion + until_date = data.pop('until_date', None) + if until_date and until_date != 946684800: + pytest.fail('Naive until_date was not interpreted as UTC') + + if bot_method_name in ['get_file', 'getFile']: + # The get_file methods try to check if the result is a local file + return File(file_id='result', file_unique_id='result').to_dict() + + method = getattr(raw_bot, bot_method_name) + signature = inspect.signature(method) + kwargs_need_default = [ + kwarg + for kwarg, value in signature.parameters.items() + if isinstance(value.default, DefaultValue) + ] + monkeypatch.setattr(raw_bot.request, 'post', make_assertion) + method(**build_kwargs(inspect.signature(method), kwargs_need_default)) + def test_ext_bot_signature(self): """ Here we make sure that all methods of ext.ExtBot have the same signature as the @@ -269,7 +331,9 @@ def test_ext_bot_signature(self): """ # Some methods of ext.ExtBot global_extra_args = set() - extra_args_per_method = defaultdict(set, {'__init__': {'arbitrary_callback_data'}}) + extra_args_per_method = defaultdict( + set, {'__init__': {'arbitrary_callback_data', 'defaults'}} + ) different_hints_per_method = defaultdict(set, {'__setattr__': {'ext_bot'}}) for name, method in inspect.getmembers(Bot, predicate=inspect.isfunction): @@ -2381,18 +2445,6 @@ def post(*args, **kwargs): bot.callback_data_cache.clear_callback_data() bot.callback_data_cache.clear_callback_queries() - @pytest.mark.parametrize( - 'cls,warn', [(Bot, True), (BotSubClass, True), (ExtBot, False), (ExtBotSubClass, False)] - ) - def test_defaults_warning(self, bot, recwarn, cls, warn): - defaults = Defaults() - cls(bot.token, defaults=defaults) - if warn: - assert len(recwarn) == 1 - assert 'Passing Defaults to telegram.Bot is deprecated.' in str(recwarn[-1].message) - else: - assert len(recwarn) == 0 - def test_camel_case_redefinition_extbot(self): invalid_camel_case_functions = [] for function_name, function in ExtBot.__dict__.items(): diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 63fab91a896..efdb52657f3 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -194,7 +194,7 @@ def mock_async_err_handler(*args, **kwargs): self.count = 5 # set defaults value to dp.bot - dp.bot.defaults = Defaults(run_async=run_async) + dp.bot._defaults = Defaults(run_async=run_async) try: dp.add_handler(MessageHandler(Filters.all, self.callback_raise_error)) dp.add_error_handler(self.error_handler_context) @@ -206,7 +206,7 @@ def mock_async_err_handler(*args, **kwargs): finally: # reset dp.bot.defaults values - dp.bot.defaults = None + dp.bot._defaults = None @pytest.mark.parametrize( ['run_async', 'expected_output'], [(True, 'running async'), (False, None)] @@ -216,7 +216,7 @@ def mock_run_async(*args, **kwargs): self.received = 'running async' # set defaults value to dp.bot - dp.bot.defaults = Defaults(run_async=run_async) + dp.bot._defaults = Defaults(run_async=run_async) try: dp.add_handler(MessageHandler(Filters.all, lambda u, c: None)) monkeypatch.setattr(dp, 'run_async', mock_run_async) @@ -225,7 +225,7 @@ def mock_run_async(*args, **kwargs): finally: # reset defaults value - dp.bot.defaults = None + dp.bot._defaults = None def test_run_async_multiple(self, bot, dp, dp2): def get_dispatcher_name(q): @@ -822,7 +822,7 @@ def dummy_callback(*args, **kwargs): dp.process_update(update) assert self.count == 0 - dp.bot.defaults = Defaults(run_async=True) + dp.bot._defaults = Defaults(run_async=True) try: for group in range(5): dp.add_handler(MessageHandler(Filters.text, dummy_callback), group=group) @@ -831,7 +831,7 @@ def dummy_callback(*args, **kwargs): dp.process_update(update) assert self.count == 0 finally: - dp.bot.defaults = None + dp.bot._defaults = None @pytest.mark.parametrize('run_async', [DEFAULT_FALSE, False]) def test_update_persistence_one_sync(self, monkeypatch, dp, run_async): @@ -864,7 +864,7 @@ def dummy_callback(*args, **kwargs): monkeypatch.setattr(dp, 'update_persistence', update_persistence) monkeypatch.setattr(dp, 'run_async', dummy_callback) - dp.bot.defaults = Defaults(run_async=run_async) + dp.bot._defaults = Defaults(run_async=run_async) try: for group in range(5): @@ -874,7 +874,7 @@ def dummy_callback(*args, **kwargs): dp.process_update(update) assert self.count == expected finally: - dp.bot.defaults = None + dp.bot._defaults = None def test_custom_context_init(self, bot): cc = ContextTypes( diff --git a/tests/test_message.py b/tests/test_message.py index 5203510ed27..37bb18d7925 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -1484,7 +1484,7 @@ def make_assertion(*args, **kwargs): assert message.unpin() def test_default_quote(self, message): - message.bot.defaults = Defaults() + message.bot._defaults = Defaults() try: message.bot.defaults._quote = False @@ -1500,7 +1500,7 @@ def test_default_quote(self, message): message.chat.type = Chat.GROUP assert message._quote(None, None) finally: - message.bot.defaults = None + message.bot._defaults = None def test_equality(self): id_ = 1 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy