File: test_official.py

package info (click to toggle)
python-telegram-bot 22.3-1
  • links: PTS
  • area: main
  • in suites: sid
  • size: 11,060 kB
  • sloc: python: 90,298; makefile: 176; sh: 4
file content (193 lines) | stat: -rw-r--r-- 7,865 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2025
# 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}."