mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2024-11-21 22:56:38 +01:00
Reduce Code Duplication in Testing Defaults
(#3419)
This commit is contained in:
parent
9c3053b3f9
commit
ff645c6fe2
33 changed files with 537 additions and 502 deletions
420
tests/auxil/bot_method_checks.py
Normal file
420
tests/auxil/bot_method_checks.py
Normal file
|
@ -0,0 +1,420 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2022
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# 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
|
||||
import os
|
||||
from typing import Any, Callable, Dict, Iterable, List
|
||||
|
||||
import pytest
|
||||
|
||||
from telegram import (
|
||||
Bot,
|
||||
ChatPermissions,
|
||||
File,
|
||||
InlineQueryResultArticle,
|
||||
InlineQueryResultCachedPhoto,
|
||||
InputMediaPhoto,
|
||||
InputTextMessageContent,
|
||||
TelegramObject,
|
||||
)
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
||||
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(
|
||||
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: Bot,
|
||||
return_value=None,
|
||||
) -> bool:
|
||||
"""
|
||||
Checks that tg.ext.Defaults are handled correctly.
|
||||
|
||||
Args:
|
||||
method: The shortcut/bot_method
|
||||
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. If this is a `TelegramObject`, Bot._post will
|
||||
return the `to_dict` representation of it.
|
||||
|
||||
"""
|
||||
raw_bot = not isinstance(bot, ExtBot)
|
||||
get_updates = method.__name__.lower().replace("_", "") == "getupdates"
|
||||
|
||||
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
|
||||
if isinstance(return_value, TelegramObject):
|
||||
return return_value.to_dict()
|
||||
return return_value
|
||||
|
||||
request = bot._request[0] if get_updates else bot.request
|
||||
orig_post = request.post
|
||||
try:
|
||||
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,
|
||||
kwargs_need_default,
|
||||
)
|
||||
assertion_callback = functools.partial(make_assertion, df_value=default_value)
|
||||
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(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(request, "post", assertion_callback)
|
||||
assert await method(**kwargs) in expected_return_values
|
||||
except Exception as exc:
|
||||
raise exc
|
||||
finally:
|
||||
setattr(request, "post", orig_post)
|
||||
if not raw_bot:
|
||||
bot._defaults = None
|
||||
|
||||
return True
|
25
tests/auxil/object_conversions.py
Normal file
25
tests/auxil/object_conversions.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2022
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# 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"
|
|
@ -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 @@ from telegram import (
|
|||
Bot,
|
||||
CallbackQuery,
|
||||
Chat,
|
||||
ChatPermissions,
|
||||
ChosenInlineResult,
|
||||
File,
|
||||
InlineQuery,
|
||||
InlineQueryResultArticle,
|
||||
InlineQueryResultCachedPhoto,
|
||||
InputMediaPhoto,
|
||||
InputTextMessageContent,
|
||||
Message,
|
||||
MessageEntity,
|
||||
PreCheckoutQuery,
|
||||
|
@ -48,14 +40,14 @@ from telegram import (
|
|||
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
|
||||
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
|
||||
|
||||
|
||||
|
@ -75,15 +67,6 @@ if GITHUB_ACTION:
|
|||
# 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
|
||||
|
@ -504,367 +487,6 @@ 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(
|
||||
ip: str,
|
||||
port: int,
|
||||
|
|
|
@ -25,12 +25,12 @@ from telegram import Animation, Bot, InputFile, MessageEntity, PhotoSize, Voice
|
|||
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")
|
||||
|
|
|
@ -38,7 +38,8 @@ from telegram.ext import (
|
|||
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))
|
||||
|
||||
|
|
|
@ -25,12 +25,12 @@ from telegram import Audio, Bot, InputFile, MessageEntity, Voice
|
|||
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")
|
||||
|
|
|
@ -38,14 +38,12 @@ from telegram import (
|
|||
ChatAdministratorRights,
|
||||
ChatPermissions,
|
||||
Dice,
|
||||
File,
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
InlineQueryResultArticle,
|
||||
InlineQueryResultDocument,
|
||||
InlineQueryResultVoice,
|
||||
InputFile,
|
||||
InputMedia,
|
||||
InputMessageContent,
|
||||
InputTextMessageContent,
|
||||
LabeledPrice,
|
||||
|
@ -64,7 +62,7 @@ from telegram import (
|
|||
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,15 +74,9 @@ from telegram.error import BadRequest, InvalidToken, NetworkError
|
|||
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 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):
|
||||
|
@ -449,77 +441,18 @@ class TestBot:
|
|||
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.
|
||||
return_value = bot.bot
|
||||
else:
|
||||
return_value = None
|
||||
|
||||
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))
|
||||
# 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(raw_bot_method, raw_bot, return_value=return_value)
|
||||
|
||||
def test_ext_bot_signature(self):
|
||||
"""
|
||||
|
|
|
@ -28,7 +28,7 @@ from telegram import CallbackQuery, Chat, InlineKeyboardButton, InlineKeyboardMa
|
|||
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")
|
||||
|
|
|
@ -22,7 +22,11 @@ from datetime import datetime
|
|||
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"])
|
||||
|
|
|
@ -22,7 +22,11 @@ import pytest
|
|||
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")
|
||||
|
|
|
@ -22,7 +22,11 @@ import pytest
|
|||
|
||||
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")
|
||||
|
|
|
@ -25,13 +25,12 @@ import pytest
|
|||
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")
|
||||
|
|
|
@ -26,7 +26,7 @@ from telegram._utils import datetime as tg_dtm
|
|||
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),
|
||||
|
|
|
@ -25,7 +25,7 @@ import pytest
|
|||
|
||||
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))
|
||||
|
||||
|
|
|
@ -25,12 +25,12 @@ from telegram import Bot, Document, InputFile, MessageEntity, PhotoSize, Voice
|
|||
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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -27,7 +27,7 @@ import time
|
|||
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))
|
||||
|
||||
|
|
|
@ -55,7 +55,11 @@ from telegram import (
|
|||
)
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import os
|
|||
|
||||
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"
|
||||
|
|
|
@ -32,7 +32,7 @@ import pytest
|
|||
|
||||
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))
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ from bs4 import BeautifulSoup
|
|||
|
||||
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 = {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -25,13 +25,12 @@ from telegram import Bot, InputFile, MessageEntity, PhotoSize, Sticker
|
|||
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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -36,7 +36,7 @@ from telegram.constants import ParseMode
|
|||
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))
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ from telegram.error import (
|
|||
)
|
||||
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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -25,12 +25,12 @@ import pytest
|
|||
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")
|
||||
|
|
|
@ -31,10 +31,10 @@ from telegram._utils.defaultvalue import DEFAULT_NONE
|
|||
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,
|
||||
|
|
|
@ -20,7 +20,11 @@ import pytest
|
|||
|
||||
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")
|
||||
|
|
|
@ -25,12 +25,12 @@ from telegram import Bot, InputFile, MessageEntity, PhotoSize, Video, Voice
|
|||
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")
|
||||
|
|
|
@ -24,12 +24,12 @@ import pytest
|
|||
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")
|
||||
|
|
|
@ -25,12 +25,12 @@ from telegram import Audio, Bot, InputFile, MessageEntity, Voice
|
|||
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")
|
||||
|
|
Loading…
Reference in a new issue