Reduce Code Duplication in Testing Defaults (#3419)

This commit is contained in:
Bibo-Joshi 2022-12-12 10:51:33 +01:00 committed by GitHub
parent 9c3053b3f9
commit ff645c6fe2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 537 additions and 502 deletions

View 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

View 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"

View file

@ -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,

View file

@ -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")

View file

@ -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))

View file

@ -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")

View file

@ -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):
"""

View file

@ -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")

View file

@ -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"])

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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),

View file

@ -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))

View file

@ -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")

View file

@ -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")

View file

@ -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))

View file

@ -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

View file

@ -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"

View file

@ -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))

View file

@ -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 = {

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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))

View file

@ -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

View file

@ -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")

View file

@ -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")

View file

@ -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,

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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")