mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2024-12-22 22:45:09 +01:00
194 lines
7.7 KiB
Python
194 lines
7.7 KiB
Python
|
#!/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}."
|