Skip to content

Commit 4b3b0cb

Browse files
authored
Merge pull request python-telegram-bot#668 from python-telegram-bot/official-test
Add an "official test"
2 parents 3ea16cb + 9f1b63b commit 4b3b0cb

File tree

4 files changed

+164
-1
lines changed

4 files changed

+164
-1
lines changed

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ flaky
66
yapf
77
pre-commit
88
pre-commit-hooks
9+
beautifulsoup4

telegram/bot.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,8 @@ def send_video(self,
534534
reply_to_message_id=None,
535535
reply_markup=None,
536536
timeout=20.,
537+
width=None,
538+
height=None,
537539
**kwargs):
538540
"""Use this method to send video files, Telegram clients support mp4
539541
videos (other formats may be sent as telegram.Document).
@@ -544,6 +546,8 @@ def send_video(self,
544546
already on the Telegram servers, or upload a new video file using
545547
multipart/form-data.
546548
duration (Optional[int]): Duration of sent video in seconds.
549+
width (Optional[int)): Video width.
550+
height (Optional[int]): Video height.
547551
caption (Optional[str]): Video caption (may also be used when resending videos by
548552
file_id).
549553
disable_notification (Optional[bool]): Sends the message silently. iOS users will not
@@ -570,6 +574,10 @@ def send_video(self,
570574
data['duration'] = duration
571575
if caption:
572576
data['caption'] = caption
577+
if width:
578+
data['width'] = width
579+
if height:
580+
data['height'] = height
573581

574582
return url, data
575583

telegram/message.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def __init__(self,
147147
successful_payment=None,
148148
bot=None,
149149
video_note=None,
150+
game=None,
150151
**kwargs):
151152
# Required
152153
self.message_id = int(message_id)
@@ -162,7 +163,7 @@ def __init__(self,
162163
self.text = text
163164
self.entities = entities or list()
164165
self.audio = audio
165-
self.game = kwargs.get('game')
166+
self.game = game
166167
self.document = document
167168
self.photo = photo
168169
self.sticker = sticker

tests/test_official.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import sys
2+
import inspect
3+
import warnings
4+
from collections import namedtuple
5+
6+
import certifi
7+
import logging
8+
from bs4 import BeautifulSoup
9+
10+
sys.path.append('.')
11+
12+
from telegram.vendor.ptb_urllib3 import urllib3
13+
import telegram
14+
15+
IGNORED_OBJECTS = ('ResponseParameters', 'CallbackGame')
16+
IGNORED_PARAMETERS = {'self', 'args', 'kwargs', 'read_latency', 'network_delay', 'timeout', 'bot'}
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
def find_next_sibling_until(tag, name, until):
22+
for sibling in tag.next_siblings:
23+
if sibling is until:
24+
return
25+
if sibling.name == name:
26+
return sibling
27+
28+
29+
def parse_table(h4):
30+
table = find_next_sibling_until(h4, 'table', h4.find_next_sibling('h4'))
31+
if not table:
32+
return []
33+
head = [td.text for td in table.tr.find_all('td')]
34+
row = namedtuple('{}TableRow'.format(h4.text), ','.join(head))
35+
t = []
36+
for tr in table.find_all('tr')[1:]:
37+
t.append(row(*[td.text for td in tr.find_all('td')]))
38+
return t
39+
40+
41+
def check_method(h4):
42+
name = h4.text
43+
method = getattr(telegram.Bot, name)
44+
table = parse_table(h4)
45+
46+
# Check arguments based on source
47+
sig = inspect.signature(method, follow_wrapped=True)
48+
49+
checked = []
50+
for parameter in table:
51+
param = sig.parameters.get(parameter.Parameters)
52+
logger.debug(parameter)
53+
assert param is not None
54+
# TODO: Check type via docstring
55+
# TODO: Check if optional or required
56+
checked.append(parameter.Parameters)
57+
58+
ignored = IGNORED_PARAMETERS.copy()
59+
if name == 'getUpdates':
60+
ignored -= {'timeout'} # Has it's own timeout parameter that we do wanna check for
61+
elif name == 'sendDocument':
62+
ignored |= {'filename'} # Undocumented
63+
elif name == 'setGameScore':
64+
ignored |= {'edit_message'} # TODO: Now deprecated, so no longer in telegrams docs
65+
66+
logger.debug((sig.parameters.keys(), checked, ignored,
67+
sig.parameters.keys() - checked - ignored))
68+
assert (sig.parameters.keys() ^ checked) - ignored == set()
69+
70+
71+
def check_object(h4):
72+
name = h4.text
73+
obj = getattr(telegram, name)
74+
table = parse_table(h4)
75+
76+
# Check arguments based on source
77+
sig = inspect.signature(obj, follow_wrapped=True)
78+
79+
checked = []
80+
for parameter in table:
81+
field = parameter.Field
82+
if field == 'from':
83+
field = 'from_user'
84+
elif name.startswith('InlineQueryResult') and field == 'type':
85+
continue
86+
elif field == 'remove_keyboard':
87+
continue
88+
89+
param = sig.parameters.get(field)
90+
logger.debug(parameter)
91+
assert param is not None
92+
# TODO: Check type via docstring
93+
# TODO: Check if optional or required
94+
checked.append(field)
95+
96+
ignored = IGNORED_PARAMETERS.copy()
97+
if name == 'InputFile':
98+
ignored |= {'data'}
99+
elif name == 'InlineQueryResult':
100+
ignored |= {'id'}
101+
elif name == 'User':
102+
ignored |= {'type'} # TODO: Deprecation
103+
104+
if name.startswith('InlineQueryResult'):
105+
ignored |= {'type'}
106+
107+
logger.debug((sig.parameters.keys(), checked, ignored,
108+
sig.parameters.keys() - checked - ignored))
109+
assert (sig.parameters.keys() ^ checked) - ignored == set()
110+
111+
112+
def test_official():
113+
if not sys.version_info >= (3, 5):
114+
warnings.warn('Not running tests, since follow_wrapped is not supported on this platform'
115+
'(python version >= 3.5 required)')
116+
return
117+
118+
http = urllib3.PoolManager(
119+
cert_reqs='CERT_REQUIRED',
120+
ca_certs=certifi.where())
121+
request = http.request('GET', 'https://core.telegram.org/bots/api')
122+
soup = BeautifulSoup(request.data.decode('utf-8'), 'html.parser')
123+
124+
for thing in soup.select('h4 > a.anchor'):
125+
# Methods and types don't have spaces in them, luckily all other sections of the docs do
126+
# TODO: don't depend on that
127+
if '-' not in thing['name']:
128+
h4 = thing.parent
129+
name = h4.text
130+
131+
test = None
132+
# Is it a method
133+
if h4.text[0].lower() == h4.text[0]:
134+
test = check_method
135+
else: # Or a type/object
136+
if name not in IGNORED_OBJECTS:
137+
test = check_object
138+
139+
if test:
140+
def fn():
141+
return test(h4)
142+
fn.description = '{}({}) ({})'.format(test.__name__, h4.text, __name__)
143+
yield fn
144+
145+
146+
if __name__ == '__main__':
147+
# Since we don't have the nice unittest asserts which show the comparison
148+
# We turn on debugging
149+
logging.basicConfig(
150+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
151+
level=logging.DEBUG)
152+
for f in test_official():
153+
f()

0 commit comments

Comments
 (0)