python-telegram-bot/tests/test_official/test_official.py

194 lines
7.7 KiB
Python
Raw Normal View History

#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# 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 inspect
from typing import TYPE_CHECKING
import pytest
import telegram
from tests.auxil.envvars import RUN_TEST_OFFICIAL
from tests.test_official.arg_type_checker import (
check_defaults_type,
check_param_type,
check_required_param,
)
from tests.test_official.exceptions import (
GLOBALLY_IGNORED_PARAMETERS,
backwards_compat_kwargs,
ptb_extra_params,
ptb_ignored_params,
)
from tests.test_official.scraper import Scraper, TelegramClass, TelegramMethod
if TYPE_CHECKING:
from types import FunctionType
# Will skip all tests in this file if the env var is False
pytestmark = pytest.mark.skipif(not RUN_TEST_OFFICIAL, reason="test_official is not enabled")
methods, method_ids, classes, class_ids = [], [], [], [] # not needed (just for completeness)
if RUN_TEST_OFFICIAL:
scraper = Scraper()
methods, method_ids = scraper.collect_methods()
classes, class_ids = scraper.collect_classes()
@pytest.mark.parametrize("tg_method", argvalues=methods, ids=method_ids)
def test_check_method(tg_method: TelegramMethod) -> None:
"""This function checks for the following things compared to the official API docs:
- Method existence
- Parameter existence
- Parameter requirement correctness
- Parameter type annotation existence
- Parameter type annotation correctness
- Parameter default value correctness
- No unexpected parameters
- Extra parameters should be keyword only
"""
ptb_method: FunctionType | None = getattr(telegram.Bot, tg_method.method_name, None)
assert ptb_method, f"Method {tg_method.method_name} not found in telegram.Bot"
# Check arguments based on source
sig = inspect.signature(ptb_method, follow_wrapped=True)
checked = []
for tg_parameter in tg_method.method_parameters:
# Check if parameter is present in our method
ptb_param = sig.parameters.get(tg_parameter.param_name)
assert (
ptb_param is not None
), f"Parameter {tg_parameter.param_name} not found in {ptb_method.__name__}"
# Now check if the parameter is required or not
assert check_required_param(
tg_parameter, ptb_param, ptb_method.__name__
), f"Param {ptb_param.name!r} of {ptb_method.__name__!r} requirement mismatch"
# Check if type annotation is present
assert (
ptb_param.annotation is not inspect.Parameter.empty
), f"Param {ptb_param.name!r} of {ptb_method.__name__!r} should have a type annotation!"
# Check if type annotation is correct
correct_type_hint, expected_type_hint = check_param_type(
ptb_param,
tg_parameter,
ptb_method,
)
assert correct_type_hint, (
f"Type hint of param {ptb_param.name!r} of {ptb_method.__name__!r} should be "
f"{expected_type_hint!r} or something else!"
)
# Now we will check that we don't pass default values if the parameter is not required.
if ptb_param.default is not inspect.Parameter.empty: # If there is a default argument...
default_arg_none = check_defaults_type(ptb_param) # check if it's None
assert (
default_arg_none
), f"Param {ptb_param.name!r} of {ptb_method.__name__!r} should be `None`"
checked.append(tg_parameter.param_name)
expected_additional_args = GLOBALLY_IGNORED_PARAMETERS.copy()
expected_additional_args |= ptb_extra_params(tg_method.method_name)
expected_additional_args |= backwards_compat_kwargs(tg_method.method_name)
unexpected_args = (sig.parameters.keys() ^ checked) - expected_additional_args
assert (
unexpected_args == set()
), f"In {ptb_method.__qualname__}, unexpected args were found: {unexpected_args}."
kw_or_positional_args = [
p.name for p in sig.parameters.values() if p.kind != inspect.Parameter.KEYWORD_ONLY
]
non_kw_only_args = set(kw_or_positional_args).difference(checked).difference(["self"])
non_kw_only_args -= backwards_compat_kwargs(tg_method.method_name)
assert non_kw_only_args == set(), (
f"In {ptb_method.__qualname__}, extra args should be keyword only (compared to "
f"{tg_method.method_name} in API)"
)
@pytest.mark.parametrize("tg_class", argvalues=classes, ids=class_ids)
def test_check_object(tg_class: TelegramClass) -> None:
"""This function checks for the following things compared to the official API docs:
- Class existence
- Parameter existence
- Parameter requirement correctness
- Parameter type annotation existence
- Parameter type annotation correctness
- Parameter default value correctness
- No unexpected parameters
"""
obj = getattr(telegram, tg_class.class_name)
# Check arguments based on source. Makes sure to only check __init__'s signature & nothing else
sig = inspect.signature(obj.__init__, follow_wrapped=True)
checked = set()
fields_removed_by_ptb = ptb_ignored_params(tg_class.class_name)
for tg_parameter in tg_class.class_parameters:
field: str = tg_parameter.param_name
if field in fields_removed_by_ptb:
continue
if field == "from":
field = "from_user"
ptb_param = sig.parameters.get(field)
assert ptb_param is not None, f"Attribute {field} not found in {obj.__name__}"
# Now check if the parameter is required or not
assert check_required_param(
tg_parameter, ptb_param, obj.__name__
), f"Param {ptb_param.name!r} of {obj.__name__!r} requirement mismatch"
# Check if type annotation is present
assert (
ptb_param.annotation is not inspect.Parameter.empty
), f"Param {ptb_param.name!r} of {obj.__name__!r} should have a type annotation"
# Check if type annotation is correct
correct_type_hint, expected_type_hint = check_param_type(ptb_param, tg_parameter, obj)
assert correct_type_hint, (
f"Type hint of param {ptb_param.name!r} of {obj.__name__!r} should be "
f"{expected_type_hint!r} or something else!"
)
# Now we will check that we don't pass default values if the parameter is not required.
if ptb_param.default is not inspect.Parameter.empty: # If there is a default argument...
default_arg_none = check_defaults_type(ptb_param) # check if its None
assert (
default_arg_none
), f"Param {ptb_param.name!r} of {obj.__name__!r} should be `None`"
checked.add(field)
expected_additional_args = GLOBALLY_IGNORED_PARAMETERS.copy()
expected_additional_args |= ptb_extra_params(tg_class.class_name)
expected_additional_args |= backwards_compat_kwargs(tg_class.class_name)
unexpected_args = (sig.parameters.keys() ^ checked) - expected_additional_args
assert (
unexpected_args == set()
), f"In {tg_class.class_name}, unexpected args were found: {unexpected_args}."