From 3b1563c8eff01392f8d0c2bd34c754b8cfcdfbc6 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 11 Dec 2022 16:33:04 +0100 Subject: [PATCH 1/6] Move auxil function to a new directory --- tests/auxil/bot_method_checks.py | 398 +++++++++++++++++++++++++++++++ tests/conftest.py | 371 +--------------------------- tests/test_animation.py | 4 +- tests/test_audio.py | 4 +- tests/test_bot.py | 10 +- tests/test_callbackquery.py | 6 +- tests/test_chat.py | 6 +- tests/test_chatjoinrequest.py | 6 +- tests/test_chatphoto.py | 5 +- tests/test_document.py | 4 +- tests/test_inlinequery.py | 6 +- tests/test_message.py | 6 +- tests/test_passportfile.py | 6 +- tests/test_photo.py | 5 +- tests/test_precheckoutquery.py | 6 +- tests/test_shippingquery.py | 6 +- tests/test_sticker.py | 4 +- tests/test_user.py | 6 +- tests/test_video.py | 4 +- tests/test_videonote.py | 4 +- tests/test_voice.py | 4 +- 21 files changed, 465 insertions(+), 406 deletions(-) create mode 100644 tests/auxil/bot_method_checks.py diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py new file mode 100644 index 00000000000..d1dea565b0a --- /dev/null +++ b/tests/auxil/bot_method_checks.py @@ -0,0 +1,398 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import datetime +import functools +import inspect +from typing import Any, Callable, Dict, Iterable, List + +import pytest +import pytz + +from telegram import ( + ChatPermissions, + File, + InlineQueryResultArticle, + InlineQueryResultCachedPhoto, + InputMediaPhoto, + InputTextMessageContent, +) +from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue +from telegram.constants import InputMediaType +from telegram.ext import Defaults, ExtBot +from telegram.request import RequestData + + +def check_shortcut_signature( + shortcut: Callable, + bot_method: Callable, + shortcut_kwargs: List[str], + additional_kwargs: List[str], +) -> bool: + """ + Checks that the signature of a shortcut matches the signature of the underlying bot method. + + Args: + shortcut: The shortcut, e.g. :meth:`telegram.Message.reply_text` + bot_method: The bot method, e.g. :meth:`telegram.Bot.send_message` + shortcut_kwargs: The kwargs passed by the shortcut directly, e.g. ``chat_id`` + additional_kwargs: Additional kwargs of the shortcut that the bot method doesn't have, e.g. + ``quote``. + + Returns: + :obj:`bool`: Whether or not the signature matches. + """ + shortcut_sig = inspect.signature(shortcut) + effective_shortcut_args = set(shortcut_sig.parameters.keys()).difference(additional_kwargs) + effective_shortcut_args.discard("self") + + bot_sig = inspect.signature(bot_method) + expected_args = set(bot_sig.parameters.keys()).difference(shortcut_kwargs) + expected_args.discard("self") + + args_check = expected_args == effective_shortcut_args + if not args_check: + raise Exception(f"Expected arguments {expected_args}, got {effective_shortcut_args}") + + # TODO: Also check annotation of return type. Would currently be a hassle b/c typing doesn't + # resolve `ForwardRef('Type')` to `Type`. For now we rely on MyPy, which probably allows the + # shortcuts to return more specific types than the bot method, but it's only annotations after + # all + for kwarg in effective_shortcut_args: + expected_kind = bot_sig.parameters[kwarg].kind + if shortcut_sig.parameters[kwarg].kind != expected_kind: + raise Exception(f"Argument {kwarg} must be of kind {expected_kind}.") + + if bot_sig.parameters[kwarg].annotation != shortcut_sig.parameters[kwarg].annotation: + if isinstance(bot_sig.parameters[kwarg].annotation, type): + if bot_sig.parameters[kwarg].annotation.__name__ != str( + shortcut_sig.parameters[kwarg].annotation + ): + raise Exception( + f"For argument {kwarg} I expected {bot_sig.parameters[kwarg].annotation}, " + f"but got {shortcut_sig.parameters[kwarg].annotation}" + ) + else: + raise Exception( + f"For argument {kwarg} I expected {bot_sig.parameters[kwarg].annotation}, but " + f"got {shortcut_sig.parameters[kwarg].annotation}" + ) + + bot_method_sig = inspect.signature(bot_method) + shortcut_sig = inspect.signature(shortcut) + for arg in expected_args: + if not shortcut_sig.parameters[arg].default == bot_method_sig.parameters[arg].default: + raise Exception( + f"Default for argument {arg} does not match the default of the Bot method." + ) + + for kwarg in additional_kwargs: + if not shortcut_sig.parameters[kwarg].kind == inspect.Parameter.KEYWORD_ONLY: + raise Exception(f"Argument {kwarg} must be a positional-only argument!") + + return True + + +async def check_shortcut_call( + shortcut_method: Callable, + bot: ExtBot, + bot_method_name: str, + skip_params: Iterable[str] = None, + shortcut_kwargs: Iterable[str] = None, +) -> bool: + """ + Checks that a shortcut passes all the existing arguments to the underlying bot method. Use as:: + + assert await check_shortcut_call(message.reply_text, message.bot, 'send_message') + + Args: + shortcut_method: The shortcut method, e.g. `message.reply_text` + bot: The bot + bot_method_name: The bot methods name, e.g. `'send_message'` + skip_params: Parameters that are allowed to be missing, e.g. `['inline_message_id']` + `rate_limit_args` will be skipped by default + shortcut_kwargs: The kwargs passed by the shortcut directly, e.g. ``chat_id`` + + Returns: + :obj:`bool` + """ + if not skip_params: + skip_params = set() + else: + skip_params = set(skip_params) + skip_params.add("rate_limit_args") + if not shortcut_kwargs: + shortcut_kwargs = set() + else: + shortcut_kwargs = set(shortcut_kwargs) + + orig_bot_method = getattr(bot, bot_method_name) + bot_signature = inspect.signature(orig_bot_method) + expected_args = set(bot_signature.parameters.keys()) - {"self"} - set(skip_params) + positional_args = { + name for name, param in bot_signature.parameters.items() if param.default == param.empty + } + ignored_args = positional_args | set(shortcut_kwargs) + + shortcut_signature = inspect.signature(shortcut_method) + # auto_pagination: Special casing for InlineQuery.answer + kwargs = {name: name for name in shortcut_signature.parameters if name != "auto_pagination"} + + async def make_assertion(**kw): + # name == value makes sure that + # a) we receive non-None input for all parameters + # b) we receive the correct input for each kwarg + received_kwargs = { + name for name, value in kw.items() if name in ignored_args or value == name + } + if not received_kwargs == expected_args: + raise Exception( + f"{orig_bot_method.__name__} did not receive correct value for the parameters " + f"{expected_args - received_kwargs}" + ) + + if bot_method_name == "get_file": + # This is here mainly for PassportFile.get_file, which calls .set_credentials on the + # return value + return File(file_id="result", file_unique_id="result") + return True + + setattr(bot, bot_method_name, make_assertion) + try: + await shortcut_method(**kwargs) + except Exception as exc: + raise exc + finally: + setattr(bot, bot_method_name, orig_bot_method) + + return True + + +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 + + +async def check_defaults_handling( + method: Callable, + bot: ExtBot, + return_value=None, +) -> bool: + """ + Checks that tg.ext.Defaults are handled correctly. + + Args: + method: The shortcut/bot_method + bot: The bot + return_value: Optional. The return value of Bot._post that the method expects. Defaults to + None. get_file is automatically handled. + + """ + + shortcut_signature = inspect.signature(method) + kwargs_need_default = [ + kwarg + for kwarg, value in shortcut_signature.parameters.items() + if isinstance(value.default, DefaultValue) and not kwarg.endswith("_timeout") + ] + + if method.__name__.endswith("_media_group"): + # the parse_mode is applied to the first media item, and we test this elsewhere + kwargs_need_default.remove("parse_mode") + + defaults_no_custom_defaults = Defaults() + 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] + + async def make_assertion( + url, request_data: RequestData, df_value=DEFAULT_NONE, *args, **kwargs + ): + data = request_data.parameters + + # Check regular arguments that need defaults + for arg in kwargs_need_default: + # 'None' should not be passed along to Telegram + if df_value in [None, DEFAULT_NONE]: + if arg in data: + pytest.fail( + f"Got value {data[arg]} for argument {arg}, expected it to be absent" + ) + else: + value = data.get(arg, "`not passed at all`") + 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: Dict): + parse_mode = m.get("parse_mode", None) + 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, dict) and isinstance(media.get("type", None), InputMediaType): + 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 + out = File(file_id="result", file_unique_id="result") + nonlocal expected_return_values + expected_return_values = [out] + return out.to_dict() + # Otherwise return None by default, as TGObject.de_json/list(None) in [None, []] + # That way we can check what gets passed to Request.post without having to actually + # make a request + # Some methods expect specific output, so we allow to customize that + return return_value + + orig_post = bot.request.post + try: + for default_value, defaults in [ + (DEFAULT_NONE, defaults_no_custom_defaults), + ("custom_default", defaults_custom_defaults), + ]: + bot._defaults = defaults + # 1: test that we get the correct default value, if we don't specify anything + kwargs = build_kwargs( + shortcut_signature, + kwargs_need_default, + ) + assertion_callback = functools.partial(make_assertion, df_value=default_value) + setattr(bot.request, "post", assertion_callback) + assert await method(**kwargs) in expected_return_values + + # 2: test that we get the manually passed non-None value + kwargs = build_kwargs(shortcut_signature, kwargs_need_default, dfv="non-None-value") + assertion_callback = functools.partial(make_assertion, df_value="non-None-value") + setattr(bot.request, "post", assertion_callback) + assert await method(**kwargs) in expected_return_values + + # 3: test that we get the manually passed None value + kwargs = build_kwargs( + shortcut_signature, + kwargs_need_default, + dfv=None, + ) + assertion_callback = functools.partial(make_assertion, df_value=None) + setattr(bot.request, "post", assertion_callback) + assert await method(**kwargs) in expected_return_values + except Exception as exc: + raise exc + finally: + setattr(bot.request, "post", orig_post) + bot._defaults = None + + return True diff --git a/tests/conftest.py b/tests/conftest.py index e05442dad98..44bf41138b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,13 +18,11 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. import asyncio import datetime -import functools -import inspect import os import re import sys from pathlib import Path -from typing import Any, Callable, Dict, Iterable, List, Optional +from typing import Callable, Optional import pytest from httpx import AsyncClient, Response @@ -33,14 +31,8 @@ Bot, CallbackQuery, Chat, - ChatPermissions, ChosenInlineResult, - File, InlineQuery, - InlineQueryResultArticle, - InlineQueryResultCachedPhoto, - InputMediaPhoto, - InputTextMessageContent, Message, MessageEntity, PreCheckoutQuery, @@ -48,9 +40,8 @@ Update, User, ) -from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue +from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ODVInput -from telegram.constants import InputMediaType from telegram.error import BadRequest, RetryAfter, TimedOut from telegram.ext import Application, ApplicationBuilder, Defaults, ExtBot, Updater from telegram.ext.filters import MessageFilter, UpdateFilter @@ -504,365 +495,7 @@ async def expect_bad_request(func, message, reason): raise e -def check_shortcut_signature( - shortcut: Callable, - bot_method: Callable, - shortcut_kwargs: List[str], - additional_kwargs: List[str], -) -> bool: - """ - Checks that the signature of a shortcut matches the signature of the underlying bot method. - - Args: - shortcut: The shortcut, e.g. :meth:`telegram.Message.reply_text` - bot_method: The bot method, e.g. :meth:`telegram.Bot.send_message` - shortcut_kwargs: The kwargs passed by the shortcut directly, e.g. ``chat_id`` - additional_kwargs: Additional kwargs of the shortcut that the bot method doesn't have, e.g. - ``quote``. - - Returns: - :obj:`bool`: Whether or not the signature matches. - """ - shortcut_sig = inspect.signature(shortcut) - effective_shortcut_args = set(shortcut_sig.parameters.keys()).difference(additional_kwargs) - effective_shortcut_args.discard("self") - - bot_sig = inspect.signature(bot_method) - expected_args = set(bot_sig.parameters.keys()).difference(shortcut_kwargs) - expected_args.discard("self") - - args_check = expected_args == effective_shortcut_args - if not args_check: - raise Exception(f"Expected arguments {expected_args}, got {effective_shortcut_args}") - - # TODO: Also check annotation of return type. Would currently be a hassle b/c typing doesn't - # resolve `ForwardRef('Type')` to `Type`. For now we rely on MyPy, which probably allows the - # shortcuts to return more specific types than the bot method, but it's only annotations after - # all - for kwarg in effective_shortcut_args: - expected_kind = bot_sig.parameters[kwarg].kind - if shortcut_sig.parameters[kwarg].kind != expected_kind: - raise Exception(f"Argument {kwarg} must be of kind {expected_kind}.") - - if bot_sig.parameters[kwarg].annotation != shortcut_sig.parameters[kwarg].annotation: - if isinstance(bot_sig.parameters[kwarg].annotation, type): - if bot_sig.parameters[kwarg].annotation.__name__ != str( - shortcut_sig.parameters[kwarg].annotation - ): - raise Exception( - f"For argument {kwarg} I expected {bot_sig.parameters[kwarg].annotation}, " - f"but got {shortcut_sig.parameters[kwarg].annotation}" - ) - else: - raise Exception( - f"For argument {kwarg} I expected {bot_sig.parameters[kwarg].annotation}, but " - f"got {shortcut_sig.parameters[kwarg].annotation}" - ) - - bot_method_sig = inspect.signature(bot_method) - shortcut_sig = inspect.signature(shortcut) - for arg in expected_args: - if not shortcut_sig.parameters[arg].default == bot_method_sig.parameters[arg].default: - raise Exception( - f"Default for argument {arg} does not match the default of the Bot method." - ) - - for kwarg in additional_kwargs: - if not shortcut_sig.parameters[kwarg].kind == inspect.Parameter.KEYWORD_ONLY: - raise Exception(f"Argument {kwarg} must be a positional-only argument!") - - return True - - -async def check_shortcut_call( - shortcut_method: Callable, - bot: ExtBot, - bot_method_name: str, - skip_params: Iterable[str] = None, - shortcut_kwargs: Iterable[str] = None, -) -> bool: - """ - Checks that a shortcut passes all the existing arguments to the underlying bot method. Use as:: - - assert await check_shortcut_call(message.reply_text, message.bot, 'send_message') - - Args: - shortcut_method: The shortcut method, e.g. `message.reply_text` - bot: The bot - bot_method_name: The bot methods name, e.g. `'send_message'` - skip_params: Parameters that are allowed to be missing, e.g. `['inline_message_id']` - `rate_limit_args` will be skipped by default - shortcut_kwargs: The kwargs passed by the shortcut directly, e.g. ``chat_id`` - - Returns: - :obj:`bool` - """ - if not skip_params: - skip_params = set() - else: - skip_params = set(skip_params) - skip_params.add("rate_limit_args") - if not shortcut_kwargs: - shortcut_kwargs = set() - else: - shortcut_kwargs = set(shortcut_kwargs) - - orig_bot_method = getattr(bot, bot_method_name) - bot_signature = inspect.signature(orig_bot_method) - expected_args = set(bot_signature.parameters.keys()) - {"self"} - set(skip_params) - positional_args = { - name for name, param in bot_signature.parameters.items() if param.default == param.empty - } - ignored_args = positional_args | set(shortcut_kwargs) - - shortcut_signature = inspect.signature(shortcut_method) - # auto_pagination: Special casing for InlineQuery.answer - kwargs = {name: name for name in shortcut_signature.parameters if name != "auto_pagination"} - - async def make_assertion(**kw): - # name == value makes sure that - # a) we receive non-None input for all parameters - # b) we receive the correct input for each kwarg - received_kwargs = { - name for name, value in kw.items() if name in ignored_args or value == name - } - if not received_kwargs == expected_args: - raise Exception( - f"{orig_bot_method.__name__} did not receive correct value for the parameters " - f"{expected_args - received_kwargs}" - ) - - if bot_method_name == "get_file": - # This is here mainly for PassportFile.get_file, which calls .set_credentials on the - # return value - return File(file_id="result", file_unique_id="result") - return True - - setattr(bot, bot_method_name, make_assertion) - try: - await shortcut_method(**kwargs) - except Exception as exc: - raise exc - finally: - setattr(bot, bot_method_name, orig_bot_method) - - 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 - - -async def check_defaults_handling( - method: Callable, - bot: ExtBot, - return_value=None, -) -> bool: - """ - Checks that tg.ext.Defaults are handled correctly. - - Args: - method: The shortcut/bot_method - bot: The bot - return_value: Optional. The return value of Bot._post that the method expects. Defaults to - None. get_file is automatically handled. - - """ - - shortcut_signature = inspect.signature(method) - kwargs_need_default = [ - kwarg - for kwarg, value in shortcut_signature.parameters.items() - if isinstance(value.default, DefaultValue) and not kwarg.endswith("_timeout") - ] - - if method.__name__.endswith("_media_group"): - # the parse_mode is applied to the first media item, and we test this elsewhere - kwargs_need_default.remove("parse_mode") - - defaults_no_custom_defaults = Defaults() - 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] - - async def make_assertion( - url, request_data: RequestData, df_value=DEFAULT_NONE, *args, **kwargs - ): - data = request_data.parameters - - # Check regular arguments that need defaults - for arg in kwargs_need_default: - # 'None' should not be passed along to Telegram - if df_value in [None, DEFAULT_NONE]: - if arg in data: - pytest.fail( - f"Got value {data[arg]} for argument {arg}, expected it to be absent" - ) - else: - value = data.get(arg, "`not passed at all`") - 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: Dict): - parse_mode = m.get("parse_mode", None) - 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, dict) and isinstance(media.get("type", None), InputMediaType): - 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 - out = File(file_id="result", file_unique_id="result") - nonlocal expected_return_values - expected_return_values = [out] - return out.to_dict() - # Otherwise return None by default, as TGObject.de_json/list(None) in [None, []] - # That way we can check what gets passed to Request.post without having to actually - # make a request - # Some methods expect specific output, so we allow to customize that - return return_value - - orig_post = bot.request.post - try: - for default_value, defaults in [ - (DEFAULT_NONE, defaults_no_custom_defaults), - ("custom_default", defaults_custom_defaults), - ]: - bot._defaults = defaults - # 1: test that we get the correct default value, if we don't specify anything - kwargs = build_kwargs( - shortcut_signature, - kwargs_need_default, - ) - assertion_callback = functools.partial(make_assertion, df_value=default_value) - setattr(bot.request, "post", assertion_callback) - assert await method(**kwargs) in expected_return_values - - # 2: test that we get the manually passed non-None value - kwargs = build_kwargs(shortcut_signature, kwargs_need_default, dfv="non-None-value") - assertion_callback = functools.partial(make_assertion, df_value="non-None-value") - setattr(bot.request, "post", assertion_callback) - assert await method(**kwargs) in expected_return_values - - # 3: test that we get the manually passed None value - kwargs = build_kwargs( - shortcut_signature, - kwargs_need_default, - dfv=None, - ) - assertion_callback = functools.partial(make_assertion, df_value=None) - setattr(bot.request, "post", assertion_callback) - assert await method(**kwargs) in expected_return_values - except Exception as exc: - raise exc - finally: - setattr(bot.request, "post", orig_post) - bot._defaults = None - - return True async def send_webhook_message( diff --git a/tests/test_animation.py b/tests/test_animation.py index c0d7c540dac..aab4c248931 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -25,12 +25,12 @@ from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData -from tests.conftest import ( +from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, check_shortcut_signature, - data_file, ) +from tests.conftest import data_file @pytest.fixture(scope="function") diff --git a/tests/test_audio.py b/tests/test_audio.py index be61f7e5a7b..f09e68f1c3f 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -25,12 +25,12 @@ from telegram.error import TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData -from tests.conftest import ( +from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, check_shortcut_signature, - data_file, ) +from tests.conftest import data_file @pytest.fixture(scope="function") diff --git a/tests/test_bot.py b/tests/test_bot.py index c59d4eec1d7..60ca1eca610 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -76,15 +76,9 @@ from telegram.ext import ExtBot, InvalidCallbackData from telegram.helpers import escape_markdown from telegram.request import BaseRequest, HTTPXRequest, RequestData +from tests.auxil.bot_method_checks import build_kwargs, check_defaults_handling from tests.bots import FALLBACKS -from tests.conftest import ( - GITHUB_ACTION, - build_kwargs, - check_defaults_handling, - data_file, - expect_bad_request, - make_bot, -) +from tests.conftest import GITHUB_ACTION, data_file, expect_bad_request, make_bot def to_camel_case(snake_str): diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 29b9b097458..108a812d31e 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -22,7 +22,11 @@ import pytest from telegram import Audio, Bot, CallbackQuery, Chat, Message, User -from tests.conftest import check_defaults_handling, check_shortcut_call, check_shortcut_signature +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) @pytest.fixture(scope="function", params=["message", "inline"]) diff --git a/tests/test_chat.py b/tests/test_chat.py index e8871deeedd..b95ca586f71 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -22,7 +22,11 @@ from telegram import Bot, Chat, ChatLocation, ChatPermissions, Location, User from telegram.constants import ChatAction, ChatType from telegram.helpers import escape_markdown -from tests.conftest import check_defaults_handling, check_shortcut_call, check_shortcut_signature +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) @pytest.fixture(scope="class") diff --git a/tests/test_chatjoinrequest.py b/tests/test_chatjoinrequest.py index c4e287150bc..b01d6f48758 100644 --- a/tests/test_chatjoinrequest.py +++ b/tests/test_chatjoinrequest.py @@ -22,7 +22,11 @@ from telegram import Bot, Chat, ChatInviteLink, ChatJoinRequest, User from telegram._utils.datetime import UTC, to_timestamp -from tests.conftest import check_defaults_handling, check_shortcut_call, check_shortcut_signature +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) @pytest.fixture(scope="class") diff --git a/tests/test_chatphoto.py b/tests/test_chatphoto.py index fd738647d6a..902402bfe69 100644 --- a/tests/test_chatphoto.py +++ b/tests/test_chatphoto.py @@ -25,13 +25,12 @@ from telegram import Bot, ChatPhoto, Voice from telegram.error import TelegramError from telegram.request import RequestData -from tests.conftest import ( +from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, check_shortcut_signature, - data_file, - expect_bad_request, ) +from tests.conftest import data_file, expect_bad_request @pytest.fixture(scope="function") diff --git a/tests/test_document.py b/tests/test_document.py index 97941efc1f9..5a46b83a896 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -25,12 +25,12 @@ from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData -from tests.conftest import ( +from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, check_shortcut_signature, - data_file, ) +from tests.conftest import data_file @pytest.fixture(scope="function") diff --git a/tests/test_inlinequery.py b/tests/test_inlinequery.py index 18a78734ab1..86d14453092 100644 --- a/tests/test_inlinequery.py +++ b/tests/test_inlinequery.py @@ -20,7 +20,11 @@ import pytest from telegram import Bot, InlineQuery, Location, Update, User -from tests.conftest import check_defaults_handling, check_shortcut_call, check_shortcut_signature +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) @pytest.fixture(scope="class") diff --git a/tests/test_message.py b/tests/test_message.py index bb8aa8519c6..97f25725a52 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -55,7 +55,11 @@ ) from telegram.constants import ChatAction, ParseMode from telegram.ext import Defaults -from tests.conftest import check_defaults_handling, check_shortcut_call, check_shortcut_signature +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) from tests.test_passport import RAW_PASSPORT_DATA diff --git a/tests/test_passportfile.py b/tests/test_passportfile.py index 1df1f6e10bb..4ec365a2266 100644 --- a/tests/test_passportfile.py +++ b/tests/test_passportfile.py @@ -19,7 +19,11 @@ import pytest from telegram import Bot, File, PassportElementError, PassportFile -from tests.conftest import check_defaults_handling, check_shortcut_call, check_shortcut_signature +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) @pytest.fixture(scope="class") diff --git a/tests/test_photo.py b/tests/test_photo.py index d5123d9af3d..ea2b364ed3f 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -25,13 +25,12 @@ from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData -from tests.conftest import ( +from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, check_shortcut_signature, - data_file, - expect_bad_request, ) +from tests.conftest import data_file, expect_bad_request @pytest.fixture(scope="function") diff --git a/tests/test_precheckoutquery.py b/tests/test_precheckoutquery.py index 6cb9d547c78..00d4250ef1f 100644 --- a/tests/test_precheckoutquery.py +++ b/tests/test_precheckoutquery.py @@ -20,7 +20,11 @@ import pytest from telegram import Bot, OrderInfo, PreCheckoutQuery, Update, User -from tests.conftest import check_defaults_handling, check_shortcut_call, check_shortcut_signature +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) @pytest.fixture(scope="class") diff --git a/tests/test_shippingquery.py b/tests/test_shippingquery.py index 01dba47c461..e6c857083aa 100644 --- a/tests/test_shippingquery.py +++ b/tests/test_shippingquery.py @@ -20,7 +20,11 @@ import pytest from telegram import Bot, ShippingAddress, ShippingQuery, Update, User -from tests.conftest import check_defaults_handling, check_shortcut_call, check_shortcut_signature +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) @pytest.fixture(scope="class") diff --git a/tests/test_sticker.py b/tests/test_sticker.py index a6e8f04722e..14b006680d6 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -25,12 +25,12 @@ from telegram import Audio, Bot, File, InputFile, MaskPosition, PhotoSize, Sticker, StickerSet from telegram.error import BadRequest, TelegramError from telegram.request import RequestData -from tests.conftest import ( +from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, check_shortcut_signature, - data_file, ) +from tests.conftest import data_file @pytest.fixture(scope="function") diff --git a/tests/test_user.py b/tests/test_user.py index 8e3849bf070..3ee9f1fd6b3 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -20,7 +20,11 @@ from telegram import Bot, InlineKeyboardButton, Update, User from telegram.helpers import escape_markdown -from tests.conftest import check_defaults_handling, check_shortcut_call, check_shortcut_signature +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) @pytest.fixture(scope="function") diff --git a/tests/test_video.py b/tests/test_video.py index 6cba616036d..876462dd636 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -25,12 +25,12 @@ from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData -from tests.conftest import ( +from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, check_shortcut_signature, - data_file, ) +from tests.conftest import data_file @pytest.fixture(scope="function") diff --git a/tests/test_videonote.py b/tests/test_videonote.py index 44855dd2873..59a62d092d8 100644 --- a/tests/test_videonote.py +++ b/tests/test_videonote.py @@ -24,12 +24,12 @@ from telegram import Bot, InputFile, PhotoSize, VideoNote, Voice from telegram.error import BadRequest, TelegramError from telegram.request import RequestData -from tests.conftest import ( +from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, check_shortcut_signature, - data_file, ) +from tests.conftest import data_file @pytest.fixture(scope="function") diff --git a/tests/test_voice.py b/tests/test_voice.py index 17024860c9f..a30145a74ce 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -25,12 +25,12 @@ from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData -from tests.conftest import ( +from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, check_shortcut_signature, - data_file, ) +from tests.conftest import data_file @pytest.fixture(scope="function") From 299721cd5f3f5116ac357ec6cb786104a70b3063 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 11 Dec 2022 17:37:12 +0100 Subject: [PATCH 2/6] expand `check_defaults_handling` to also handling telegram.Bot --- tests/auxil/bot_method_checks.py | 33 ++++++++---- tests/test_bot.py | 87 ++++++-------------------------- 2 files changed, 39 insertions(+), 81 deletions(-) diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index d1dea565b0a..8fbda03e3cc 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -25,12 +25,14 @@ import pytz from telegram import ( + Bot, ChatPermissions, File, InlineQueryResultArticle, InlineQueryResultCachedPhoto, InputMediaPhoto, InputTextMessageContent, + TelegramObject, ) from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram.constants import InputMediaType @@ -236,7 +238,7 @@ def build_kwargs(signature: inspect.Signature, default_kwargs, dfv: Any = DEFAUL async def check_defaults_handling( method: Callable, - bot: ExtBot, + bot: Bot, return_value=None, ) -> bool: """ @@ -244,11 +246,14 @@ async def check_defaults_handling( Args: method: The shortcut/bot_method - bot: The bot + bot: The bot. May be a telegram.Bot or a telegram.ext.ExtBot. In the former case, all + default values will be converted to None. return_value: Optional. The return value of Bot._post that the method expects. Defaults to - None. get_file is automatically handled. + None. get_file is automatically handled. If this is a `TelegramObject`, Bot._post will + return the `to_dict` representation of it. """ + raw_bot = not isinstance(bot, ExtBot) shortcut_signature = inspect.signature(method) kwargs_need_default = [ @@ -356,15 +361,24 @@ def check_input_media(m: Dict): # That way we can check what gets passed to Request.post without having to actually # make a request # Some methods expect specific output, so we allow to customize that + if isinstance(return_value, TelegramObject): + return return_value.to_dict() return return_value orig_post = bot.request.post try: - for default_value, defaults in [ - (DEFAULT_NONE, defaults_no_custom_defaults), - ("custom_default", defaults_custom_defaults), - ]: - bot._defaults = defaults + if raw_bot: + combinations = [(DEFAULT_NONE, None)] + else: + combinations = [ + (DEFAULT_NONE, defaults_no_custom_defaults), + ("custom_default", defaults_custom_defaults), + ] + + for default_value, defaults in combinations: + if not raw_bot: + bot._defaults = defaults + # 1: test that we get the correct default value, if we don't specify anything kwargs = build_kwargs( shortcut_signature, @@ -393,6 +407,7 @@ def check_input_media(m: Dict): raise exc finally: setattr(bot.request, "post", orig_post) - bot._defaults = None + if not raw_bot: + bot._defaults = None return True diff --git a/tests/test_bot.py b/tests/test_bot.py index 60ca1eca610..1e618baf781 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -38,14 +38,12 @@ ChatAdministratorRights, ChatPermissions, Dice, - File, InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, InlineQueryResultDocument, InlineQueryResultVoice, InputFile, - InputMedia, InputMessageContent, InputTextMessageContent, LabeledPrice, @@ -64,7 +62,7 @@ WebAppInfo, ) from telegram._utils.datetime import UTC, from_timestamp, to_timestamp -from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue +from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.constants import ( ChatAction, InlineQueryLimit, @@ -76,7 +74,7 @@ from telegram.ext import ExtBot, InvalidCallbackData from telegram.helpers import escape_markdown from telegram.request import BaseRequest, HTTPXRequest, RequestData -from tests.auxil.bot_method_checks import build_kwargs, check_defaults_handling +from tests.auxil.bot_method_checks import check_defaults_handling from tests.bots import FALLBACKS from tests.conftest import GITHUB_ACTION, data_file, expect_bad_request, make_bot @@ -446,74 +444,19 @@ async def test_defaults_handling( if bot_method_name.lower().replace("_", "") == "getupdates": return - try: - # Check that ExtBot does the right thing - bot_method = getattr(bot, bot_method_name) - assert await 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 - async def make_assertion(url, request_data: RequestData, *args, **kwargs): - json_data = request_data.parameters - - # Check regular kwargs - for k, v in json_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.get("parse_mode"), DefaultValue) for med in v): - pytest.fail("One of the media items has a DefaultValue parse_mode") - - # Check inline query results - if bot_method_name.lower().replace("_", "") == "answerinlinequery": - for result_dict in json_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 = json_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) - await method(**build_kwargs(inspect.signature(method), kwargs_need_default)) - finally: - await bot.get_me() # because running the mock-get_me messages with bot.bot & friends - - 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) - await method(**build_kwargs(inspect.signature(method), kwargs_need_default)) + if bot_method_name.lower().replace("_", "") == "getme": + # Mocking get_me within check_defaults_handling messes with the cached values like + # Bot.{bot, username, id, …}` unless we return the expected User object. + return_value = bot.bot + else: + return_value = None + + # Check that ExtBot does the right thing + bot_method = getattr(bot, bot_method_name) + assert await check_defaults_handling(bot_method, bot, return_value=return_value) + assert await check_defaults_handling( + getattr(raw_bot, bot_method_name), raw_bot, return_value=return_value + ) def test_ext_bot_signature(self): """ From 2248892cf96ef66823d3aaf7ffead9c81a661121 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 11 Dec 2022 18:10:59 +0100 Subject: [PATCH 3/6] Try fixing the tests without optional deps --- tests/auxil/bot_method_checks.py | 7 ++++++- tests/auxil/object_conversions.py | 25 +++++++++++++++++++++++++ tests/conftest.py | 10 +--------- tests/test_applicationbuilder.py | 3 ++- tests/test_callbackdatacache.py | 2 +- tests/test_datetime.py | 2 +- tests/test_defaults.py | 2 +- tests/test_jobqueue.py | 2 +- tests/test_meta.py | 2 +- tests/test_no_passport.py | 2 +- tests/test_official.py | 2 +- tests/test_ratelimiter.py | 2 +- tests/test_request.py | 2 +- tests/test_updater.py | 2 +- 14 files changed, 44 insertions(+), 21 deletions(-) create mode 100644 tests/auxil/object_conversions.py diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index 8fbda03e3cc..49c4cbfd2b7 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -19,10 +19,10 @@ import datetime import functools import inspect +import os from typing import Any, Callable, Dict, Iterable, List import pytest -import pytz from telegram import ( Bot, @@ -38,6 +38,11 @@ from telegram.constants import InputMediaType from telegram.ext import Defaults, ExtBot from telegram.request import RequestData +from tests.auxil.object_conversions import env_var_2_bool + +TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", True)) +if TEST_WITH_OPT_DEPS: + import pytz def check_shortcut_signature( diff --git a/tests/auxil/object_conversions.py b/tests/auxil/object_conversions.py new file mode 100644 index 00000000000..9df9663ef60 --- /dev/null +++ b/tests/auxil/object_conversions.py @@ -0,0 +1,25 @@ +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + + +def env_var_2_bool(env_var: object) -> bool: + if isinstance(env_var, bool): + return env_var + if not isinstance(env_var, str): + return False + return env_var.lower().strip() == "true" diff --git a/tests/conftest.py b/tests/conftest.py index 44bf41138b2..01b5509db8e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,6 +47,7 @@ from telegram.ext.filters import MessageFilter, UpdateFilter from telegram.request import RequestData from telegram.request._httpxrequest import HTTPXRequest +from tests.auxil.object_conversions import env_var_2_bool from tests.bots import get_bot @@ -66,15 +67,6 @@ def pytest_runtestloop(session): # DO NOT USE IN PRODUCTION! PRIVATE_KEY = b"-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEA0AvEbNaOnfIL3GjB8VI4M5IaWe+GcK8eSPHkLkXREIsaddum\r\nwPBm/+w8lFYdnY+O06OEJrsaDtwGdU//8cbGJ/H/9cJH3dh0tNbfszP7nTrQD+88\r\nydlcYHzClaG8G+oTe9uEZSVdDXj5IUqR0y6rDXXb9tC9l+oSz+ShYg6+C4grAb3E\r\nSTv5khZ9Zsi/JEPWStqNdpoNuRh7qEYc3t4B/a5BH7bsQENyJSc8AWrfv+drPAEe\r\njQ8xm1ygzWvJp8yZPwOIYuL+obtANcoVT2G2150Wy6qLC0bD88Bm40GqLbSazueC\r\nRHZRug0B9rMUKvKc4FhG4AlNzBCaKgIcCWEqKwIDAQABAoIBACcIjin9d3Sa3S7V\r\nWM32JyVF3DvTfN3XfU8iUzV7U+ZOswA53eeFM04A/Ly4C4ZsUNfUbg72O8Vd8rg/\r\n8j1ilfsYpHVvphwxaHQlfIMa1bKCPlc/A6C7b2GLBtccKTbzjARJA2YWxIaqk9Nz\r\nMjj1IJK98i80qt29xRnMQ5sqOO3gn2SxTErvNchtBiwOH8NirqERXig8VCY6fr3n\r\nz7ZImPU3G/4qpD0+9ULrt9x/VkjqVvNdK1l7CyAuve3D7ha3jPMfVHFtVH5gqbyp\r\nKotyIHAyD+Ex3FQ1JV+H7DkP0cPctQiss7OiO9Zd9C1G2OrfQz9el7ewAPqOmZtC\r\nKjB3hUECgYEA/4MfKa1cvaCqzd3yUprp1JhvssVkhM1HyucIxB5xmBcVLX2/Kdhn\r\nhiDApZXARK0O9IRpFF6QVeMEX7TzFwB6dfkyIePsGxputA5SPbtBlHOvjZa8omMl\r\nEYfNa8x/mJkvSEpzvkWPascuHJWv1cEypqphu/70DxubWB5UKo/8o6cCgYEA0HFy\r\ncgwPMB//nltHGrmaQZPFT7/Qgl9ErZT3G9S8teWY4o4CXnkdU75tBoKAaJnpSfX3\r\nq8VuRerF45AFhqCKhlG4l51oW7TUH50qE3GM+4ivaH5YZB3biwQ9Wqw+QyNLAh/Q\r\nnS4/Wwb8qC9QuyEgcCju5lsCaPEXZiZqtPVxZd0CgYEAshBG31yZjO0zG1TZUwfy\r\nfN3euc8mRgZpSdXIHiS5NSyg7Zr8ZcUSID8jAkJiQ3n3OiAsuq1MGQ6kNa582kLT\r\nFPQdI9Ea8ahyDbkNR0gAY9xbM2kg/Gnro1PorH9PTKE0ekSodKk1UUyNrg4DBAwn\r\nqE6E3ebHXt/2WmqIbUD653ECgYBQCC8EAQNX3AFegPd1GGxU33Lz4tchJ4kMCNU0\r\nN2NZh9VCr3nTYjdTbxsXU8YP44CCKFG2/zAO4kymyiaFAWEOn5P7irGF/JExrjt4\r\nibGy5lFLEq/HiPtBjhgsl1O0nXlwUFzd7OLghXc+8CPUJaz5w42unqT3PBJa40c3\r\nQcIPdQKBgBnSb7BcDAAQ/Qx9juo/RKpvhyeqlnp0GzPSQjvtWi9dQRIu9Pe7luHc\r\nm1Img1EO1OyE3dis/rLaDsAa2AKu1Yx6h85EmNjavBqP9wqmFa0NIQQH8fvzKY3/\r\nP8IHY6009aoamLqYaexvrkHVq7fFKiI6k8myMJ6qblVNFv14+KXU\r\n-----END RSA PRIVATE KEY-----" # noqa: E501 - -def env_var_2_bool(env_var: object) -> bool: - if isinstance(env_var, bool): - return env_var - if not isinstance(env_var, str): - return False - return env_var.lower().strip() == "true" - - TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", True)) if TEST_WITH_OPT_DEPS: import pytz diff --git a/tests/test_applicationbuilder.py b/tests/test_applicationbuilder.py index da6be21a98b..148a28a0464 100644 --- a/tests/test_applicationbuilder.py +++ b/tests/test_applicationbuilder.py @@ -38,7 +38,8 @@ from telegram.ext._applicationbuilder import _BOT_CHECKS from telegram.request import HTTPXRequest -from .conftest import PRIVATE_KEY, data_file, env_var_2_bool +from .auxil.object_conversions import env_var_2_bool +from .conftest import PRIVATE_KEY, data_file TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", True)) diff --git a/tests/test_callbackdatacache.py b/tests/test_callbackdatacache.py index 8cb3abc6a1d..f7fdb5eb751 100644 --- a/tests/test_callbackdatacache.py +++ b/tests/test_callbackdatacache.py @@ -28,7 +28,7 @@ from telegram._utils.datetime import UTC from telegram.ext import ExtBot from telegram.ext._callbackdatacache import CallbackDataCache, InvalidCallbackData, _KeyboardData -from tests.conftest import env_var_2_bool +from tests.auxil.object_conversions import env_var_2_bool @pytest.fixture(scope="function") diff --git a/tests/test_datetime.py b/tests/test_datetime.py index b8f59282d58..aae6ccac0af 100644 --- a/tests/test_datetime.py +++ b/tests/test_datetime.py @@ -26,7 +26,7 @@ from telegram.ext import Defaults # sample time specification values categorised into absolute / delta / time-of-day -from tests.conftest import env_var_2_bool +from tests.auxil.object_conversions import env_var_2_bool ABSOLUTE_TIME_SPECS = [ dtm.datetime.now(tz=dtm.timezone(dtm.timedelta(hours=-7))).replace(second=0, microsecond=0), diff --git a/tests/test_defaults.py b/tests/test_defaults.py index b596e377f25..902753b05d2 100644 --- a/tests/test_defaults.py +++ b/tests/test_defaults.py @@ -25,7 +25,7 @@ from telegram import User from telegram.ext import Defaults -from tests.conftest import env_var_2_bool +from tests.auxil.object_conversions import env_var_2_bool TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", True)) diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index be691fc1a25..5488eb476b8 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -27,7 +27,7 @@ import pytest from telegram.ext import ApplicationBuilder, CallbackContext, ContextTypes, Job, JobQueue -from tests.conftest import env_var_2_bool +from tests.auxil.object_conversions import env_var_2_bool TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", True)) diff --git a/tests/test_meta.py b/tests/test_meta.py index cd0d173726b..fe412a96b41 100644 --- a/tests/test_meta.py +++ b/tests/test_meta.py @@ -20,7 +20,7 @@ import pytest -from tests.conftest import env_var_2_bool +from tests.auxil.object_conversions import env_var_2_bool skip_disabled = pytest.mark.skipif( not env_var_2_bool(os.getenv("TEST_BUILD", False)), reason="TEST_BUILD not enabled" diff --git a/tests/test_no_passport.py b/tests/test_no_passport.py index 721f6db793f..071e29c5306 100644 --- a/tests/test_no_passport.py +++ b/tests/test_no_passport.py @@ -32,7 +32,7 @@ from telegram import _bot as bot from telegram._passport import credentials as credentials -from tests.conftest import env_var_2_bool +from tests.auxil.object_conversions import env_var_2_bool TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", True)) diff --git a/tests/test_official.py b/tests/test_official.py index 9ac1907e21f..8c38e93227e 100644 --- a/tests/test_official.py +++ b/tests/test_official.py @@ -26,7 +26,7 @@ import telegram from telegram._utils.defaultvalue import DefaultValue -from tests.conftest import env_var_2_bool +from tests.auxil.object_conversions import env_var_2_bool IGNORED_OBJECTS = ("ResponseParameters", "CallbackGame") IGNORED_PARAMETERS = { diff --git a/tests/test_ratelimiter.py b/tests/test_ratelimiter.py index dc909af34c8..39c4c731fc9 100644 --- a/tests/test_ratelimiter.py +++ b/tests/test_ratelimiter.py @@ -36,7 +36,7 @@ from telegram.error import RetryAfter from telegram.ext import AIORateLimiter, BaseRateLimiter, Defaults, ExtBot from telegram.request import BaseRequest, RequestData -from tests.conftest import env_var_2_bool +from tests.auxil.object_conversions import env_var_2_bool TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", True)) diff --git a/tests/test_request.py b/tests/test_request.py index b38f52bdbad..7ab655586cc 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -43,7 +43,7 @@ ) from telegram.request._httpxrequest import HTTPXRequest -from .conftest import env_var_2_bool +from .auxil.object_conversions import env_var_2_bool # We only need the first fixture, but it uses the others, so pytest needs us to import them as well from .test_requestdata import ( # noqa: F401 diff --git a/tests/test_updater.py b/tests/test_updater.py index 998d253fbc8..62fb0da5293 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -31,10 +31,10 @@ from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut from telegram.ext import ExtBot, InvalidCallbackData, Updater from telegram.request import HTTPXRequest +from tests.auxil.object_conversions import env_var_2_bool from tests.conftest import ( DictBot, data_file, - env_var_2_bool, make_bot, make_message, make_message_update, From 6b5fed39deb5a56401b47ea4c0d4a4bb8dd9cc0a Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 11 Dec 2022 20:29:44 +0100 Subject: [PATCH 4/6] get started on review --- tests/auxil/bot_method_checks.py | 4 +--- tests/conftest.py | 3 --- tests/test_bot.py | 5 ++--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index 49c4cbfd2b7..73ef5e60933 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -19,10 +19,10 @@ import datetime import functools import inspect -import os from typing import Any, Callable, Dict, Iterable, List import pytest +from conftest import TEST_WITH_OPT_DEPS from telegram import ( Bot, @@ -38,9 +38,7 @@ from telegram.constants import InputMediaType from telegram.ext import Defaults, ExtBot from telegram.request import RequestData -from tests.auxil.object_conversions import env_var_2_bool -TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", True)) if TEST_WITH_OPT_DEPS: import pytz diff --git a/tests/conftest.py b/tests/conftest.py index 01b5509db8e..19e8828e131 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -487,9 +487,6 @@ async def expect_bad_request(func, message, reason): raise e -# mainly for check_defaults_handling below - - async def send_webhook_message( ip: str, port: int, diff --git a/tests/test_bot.py b/tests/test_bot.py index 1e618baf781..dd4e67dfb02 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -453,10 +453,9 @@ async def test_defaults_handling( # Check that ExtBot does the right thing bot_method = getattr(bot, bot_method_name) + raw_bot_method = getattr(raw_bot, bot_method_name) assert await check_defaults_handling(bot_method, bot, return_value=return_value) - assert await check_defaults_handling( - getattr(raw_bot, bot_method_name), raw_bot, return_value=return_value - ) + assert await check_defaults_handling(raw_bot_method, raw_bot, return_value=return_value) def test_ext_bot_signature(self): """ From e75bd61c10abfbe734c5e017a623539c2150d238 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 11 Dec 2022 20:55:14 +0100 Subject: [PATCH 5/6] test defaults handling in get_updates as well --- tests/auxil/bot_method_checks.py | 12 +++++++----- tests/test_bot.py | 3 --- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index 73ef5e60933..251cd88cac7 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -257,6 +257,7 @@ async def check_defaults_handling( """ raw_bot = not isinstance(bot, ExtBot) + get_updates = method.__name__.lower().replace("_", "") == "getupdates" shortcut_signature = inspect.signature(method) kwargs_need_default = [ @@ -368,7 +369,8 @@ def check_input_media(m: Dict): return return_value.to_dict() return return_value - orig_post = bot.request.post + request = bot._request[0] if get_updates else bot.request + orig_post = request.post try: if raw_bot: combinations = [(DEFAULT_NONE, None)] @@ -388,13 +390,13 @@ def check_input_media(m: Dict): kwargs_need_default, ) assertion_callback = functools.partial(make_assertion, df_value=default_value) - setattr(bot.request, "post", assertion_callback) + setattr(request, "post", assertion_callback) assert await method(**kwargs) in expected_return_values # 2: test that we get the manually passed non-None value kwargs = build_kwargs(shortcut_signature, kwargs_need_default, dfv="non-None-value") assertion_callback = functools.partial(make_assertion, df_value="non-None-value") - setattr(bot.request, "post", assertion_callback) + setattr(request, "post", assertion_callback) assert await method(**kwargs) in expected_return_values # 3: test that we get the manually passed None value @@ -404,12 +406,12 @@ def check_input_media(m: Dict): dfv=None, ) assertion_callback = functools.partial(make_assertion, df_value=None) - setattr(bot.request, "post", assertion_callback) + setattr(request, "post", assertion_callback) assert await method(**kwargs) in expected_return_values except Exception as exc: raise exc finally: - setattr(bot.request, "post", orig_post) + setattr(request, "post", orig_post) if not raw_bot: bot._defaults = None diff --git a/tests/test_bot.py b/tests/test_bot.py index dd4e67dfb02..3d07adf8c38 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -441,9 +441,6 @@ async def test_defaults_handling( 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. """ - if bot_method_name.lower().replace("_", "") == "getupdates": - return - if bot_method_name.lower().replace("_", "") == "getme": # Mocking get_me within check_defaults_handling messes with the cached values like # Bot.{bot, username, id, …}` unless we return the expected User object. From e81b5647bdce206d50a9a6f0fa406f5ffd02bc6a Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 11 Dec 2022 20:57:13 +0100 Subject: [PATCH 6/6] party revert review --- tests/auxil/bot_method_checks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index 251cd88cac7..35d750c603e 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -19,10 +19,10 @@ import datetime import functools import inspect +import os from typing import Any, Callable, Dict, Iterable, List import pytest -from conftest import TEST_WITH_OPT_DEPS from telegram import ( Bot, @@ -38,7 +38,9 @@ from telegram.constants import InputMediaType from telegram.ext import Defaults, ExtBot from telegram.request import RequestData +from tests.auxil.object_conversions import env_var_2_bool +TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", True)) if TEST_WITH_OPT_DEPS: import pytz