support 3.4 API (#865)

This commit is contained in:
Eldinnie 2017-10-14 20:03:02 +02:00 committed by Noam Meltzer
parent 8a8b1215c8
commit bfad2fa1f3
13 changed files with 403 additions and 35 deletions

View file

@ -96,7 +96,7 @@ make the development of bots easy and straightforward. These classes are contain
Telegram API support
====================
As of **23. July 2017**, all types and methods of the Telegram Bot API 3.2 are supported.
All types and methods of the Telegram Bot API 3.4 are supported.
==========
Installing

View file

@ -768,6 +768,7 @@ class Bot(TelegramObject):
reply_markup=None,
timeout=None,
location=None,
live_period=None,
**kwargs):
"""Use this method to send point on the map.
@ -780,6 +781,8 @@ class Bot(TelegramObject):
latitude (:obj:`float`, optional): Latitude of location.
longitude (:obj:`float`, optional): Longitude of location.
location (:class:`telegram.Location`, optional): The location to send.
live_period (:obj:`int`, optional): Period in seconds for which the location will be
updated, should be between 60 and 86400.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
@ -803,7 +806,11 @@ class Bot(TelegramObject):
if not (all([latitude, longitude]) or location):
raise ValueError("Either location or latitude and longitude must be passed as"
"argument")
"argument.")
if not ((latitude is not None or longitude is not None) ^ bool(location)):
raise ValueError("Either location or latitude and longitude must be passed as"
"argument. Not both.")
if isinstance(location, Location):
latitude = location.latitude
@ -811,6 +818,114 @@ class Bot(TelegramObject):
data = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude}
if live_period:
data['live_period'] = live_period
return url, data
@log
@message
def edit_message_live_location(self,
chat_id=None,
message_id=None,
inline_message_id=None,
latitude=None,
longitude=None,
location=None,
reply_markup=None,
**kwargs):
"""Use this method to edit live location messages sent by the bot or via the bot
(for inline bots). A location can be edited until its :attr:`live_period` expires or
editing is explicitly disabled by a call to :attr:`stop_message_live_location`.
Note:
You can either supply a :obj:`latitude` and :obj:`longitude` or a :obj:`location`.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format @channelusername).
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
Identifier of the sent message.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
specified. Identifier of the inline message.
latitude (:obj:`float`, optional): Latitude of location.
longitude (:obj:`float`, optional): Longitude of location.
location (:class:`telegram.Location`, optional): The location to send.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
Returns:
:class:`telegram.Message`: On success the edited message.
"""
url = '{0}/editMessageLiveLocation'.format(self.base_url)
if not (all([latitude, longitude]) or location):
raise ValueError("Either location or latitude and longitude must be passed as"
"argument.")
if not ((latitude is not None or longitude is not None) ^ bool(location)):
raise ValueError("Either location or latitude and longitude must be passed as"
"argument. Not both.")
if isinstance(location, Location):
latitude = location.latitude
longitude = location.longitude
data = {'latitude': latitude, 'longitude': longitude}
if chat_id:
data['chat_id'] = chat_id
if message_id:
data['message_id'] = message_id
if inline_message_id:
data['inline_message_id'] = inline_message_id
return url, data
@log
@message
def stop_message_live_location(self,
chat_id=None,
message_id=None,
inline_message_id=None,
reply_markup=None,
**kwargs):
"""Use this method to stop updating a live location message sent by the bot or via the bot
(for inline bots) before live_period expires.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format @channelusername).
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
Identifier of the sent message.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
specified. Identifier of the inline message.
reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A
JSON-serialized object for an inline keyboard, custom reply keyboard, instructions
to remove reply keyboard or to force a reply from the user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
Returns:
:class:`telegram.Message`: On success the edited message.
"""
url = '{0}/stopMessageLiveLocation'.format(self.base_url)
data = {}
if chat_id:
data['chat_id'] = chat_id
if message_id:
data['message_id'] = message_id
if inline_message_id:
data['inline_message_id'] = inline_message_id
return url, data
@log
@ -1825,6 +1940,63 @@ class Bot(TelegramObject):
return ChatMember.de_json(result, self)
@log
def set_chat_sticker_set(self, chat_id, sticker_set_name, timeout=None, **kwargs):
"""Use this method to set a new group sticker set for a supergroup.
The bot must be an administrator in the chat for this to work and must have the appropriate
admin rights. Use the field :attr:`telegram.Chat.can_set_sticker_set` optionally returned
in :attr:`get_chat` requests to check if the bot can use this method.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target supergroup (in the format @supergroupusername).
sticker_set_name (:obj:`str`): Name of the sticker set to be set as the group
sticker set.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Returns:
:obj:`bool`: True on success.
"""
url = '{0}/setChatStickerSet'.format(self.base_url)
data = {'chat_id': chat_id, 'sticker_set_name': sticker_set_name}
result = self._request.post(url, data, timeout=timeout)
return result
@log
def delete_chat_sticker_set(self, chat_id, timeout=None, **kwargs):
"""Use this method to delete a group sticker set from a supergroup. The bot must be an
administrator in the chat for this to work and must have the appropriate admin rights.
Use the field :attr:`telegram.Chat.can_set_sticker_set` optionally returned in
:attr:`get_chat` requests to check if the bot can use this method.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target supergroup (in the format @supergroupusername).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Returns:
:obj:`bool`: True on success.
"""
url = '{0}/deleteChatStickerSet'.format(self.base_url)
data = {'chat_id': chat_id}
result = self._request.post(url, data, timeout=timeout)
return result
def get_webhook_info(self, timeout=None, **kwargs):
"""Use this method to get current webhook status. Requires no parameters.
@ -2794,6 +2966,8 @@ class Bot(TelegramObject):
sendVoice = send_voice
sendVideoNote = send_video_note
sendLocation = send_location
editMessageLiveLocation = edit_message_live_location
stopMessageLiveLocation = stop_message_live_location
sendVenue = send_venue
sendContact = send_contact
sendGame = send_game
@ -2814,6 +2988,8 @@ class Bot(TelegramObject):
getChat = get_chat
getChatAdministrators = get_chat_administrators
getChatMember = get_chat_member
setChatStickerSet = set_chat_sticker_set
deleteChatStickerSet = delete_chat_sticker_set
getChatMembersCount = get_chat_members_count
getWebhookInfo = get_webhook_info
setGameScore = set_game_score

View file

@ -38,6 +38,9 @@ class Chat(TelegramObject):
invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats.
pinned_message (:class:`telegram.Message`): Optional. Pinned message, for supergroups.
Returned only in get_chat.
sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set.
can_set_sticker_set (:obj:`bool`): Optional. ``True``, if the bot can change group the
sticker set.
Args:
id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits
@ -61,6 +64,10 @@ class Chat(TelegramObject):
pinned_message (:class:`telegram.Message`, optional): Pinned message, for supergroups.
Returned only in get_chat.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
sticker_set_name (:obj:`str`, optional): For supergroups, name of Group sticker set.
Returned only in get_chat.
can_set_sticker_set (:obj:`bool`, optional): ``True``, if the bot can change group the
sticker set. Returned only in get_chat.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
@ -87,6 +94,8 @@ class Chat(TelegramObject):
description=None,
invite_link=None,
pinned_message=None,
sticker_set_name=None,
can_set_sticker_set=None,
**kwargs):
# Required
self.id = int(id)
@ -101,6 +110,8 @@ class Chat(TelegramObject):
self.description = description
self.invite_link = invite_link
self.pinned_message = pinned_message
self.sticker_set_name = sticker_set_name
self.can_set_sticker_set = can_set_sticker_set
self.bot = bot
self._id_attrs = (self.id,)

View file

@ -33,6 +33,8 @@ class InlineQueryResultLocation(InlineQueryResult):
latitude (:obj:`float`): Location latitude in degrees.
longitude (:obj:`float`): Location longitude in degrees.
title (:obj:`str`): Location title.
live_period (:obj:`int`): Optional. Period in seconds for which the location can be
updated, should be between 60 and 86400.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
@ -46,6 +48,8 @@ class InlineQueryResultLocation(InlineQueryResult):
latitude (:obj:`float`): Location latitude in degrees.
longitude (:obj:`float`): Location longitude in degrees.
title (:obj:`str`): Location title.
live_period (:obj:`int`, optional): Period in seconds for which the location can be
updated, should be between 60 and 86400.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
@ -62,6 +66,7 @@ class InlineQueryResultLocation(InlineQueryResult):
latitude,
longitude,
title,
live_period=None,
reply_markup=None,
input_message_content=None,
thumb_url=None,
@ -75,6 +80,8 @@ class InlineQueryResultLocation(InlineQueryResult):
self.title = title
# Optionals
if live_period:
self.live_period = live_period
if reply_markup:
self.reply_markup = reply_markup
if input_message_content:

View file

@ -32,11 +32,14 @@ class InputLocationMessageContent(InputMessageContent):
Args:
latitude (:obj:`float`): Latitude of the location in degrees.
longitude (:obj:`float`): Longitude of the location in degrees.
live_period (:obj:`int`, optional): Period in seconds for which the location can be
updated, should be between 60 and 86400.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
def __init__(self, latitude, longitude, **kwargs):
def __init__(self, latitude, longitude, live_period=None, **kwargs):
# Required
self.latitude = latitude
self.longitude = longitude
self.live_period = live_period

View file

@ -26,7 +26,6 @@ from telegram import (Audio, Contact, Document, Chat, Location, PhotoSize, Stick
from telegram.utils.deprecate import warn_deprecate_obj
from telegram.utils.helpers import escape_html, escape_markdown, to_timestamp, from_timestamp
_UNDEFINED = object()
@ -54,6 +53,10 @@ class Message(TelegramObject):
usernames, URLs, bot commands, etc. that appear in the text. See
:attr:`Message.parse_entity` and :attr:`parse_entities` methods for how to use
properly.
caption_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like
usernames, URLs, bot commands, etc. that appear in the caption. See
:attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` methods for how
to use properly.
audio (:class:`telegram.Audio`): Optional. Information about the file.
document (:class:`telegram.Document`): Optional. Information about the file.
game (:class:`telegram.Game`): Optional. Information about the game.
@ -119,6 +122,10 @@ class Message(TelegramObject):
entities (List[:class:`telegram.MessageEntity`], optional): For text messages, special
entities like usernames, URLs, bot commands, etc. that appear in the text. See
attr:`parse_entity` and attr:`parse_entities` methods for how to use properly.
caption_entities (List[:class:`telegram.MessageEntity`]): Optional. For Messages with a
Caption. Special entities like usernames, URLs, bot commands, etc. that appear in the
caption. See :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities`
methods for how to use properly.
audio (:class:`telegram.Audio`, optional): Message is an audio file, information
about the file.
document (:class:`telegram.Document`, optional): Message is a general file, information
@ -196,6 +203,7 @@ class Message(TelegramObject):
edit_date=None,
text=None,
entities=None,
caption_entities=None,
audio=None,
document=None,
game=None,
@ -239,6 +247,7 @@ class Message(TelegramObject):
self.edit_date = edit_date
self.text = text
self.entities = entities or list()
self.caption_entities = caption_entities or list()
self.audio = audio
self.game = game
self.document = document
@ -289,6 +298,7 @@ class Message(TelegramObject):
data['date'] = from_timestamp(data['date'])
data['chat'] = Chat.de_json(data.get('chat'), bot)
data['entities'] = MessageEntity.de_list(data.get('entities'), bot)
data['caption_entities'] = MessageEntity.de_list(data.get('caption_entities'), bot)
data['forward_from'] = User.de_json(data.get('forward_from'), bot)
data['forward_from_chat'] = Chat.de_json(data.get('forward_from_chat'), bot)
data['forward_date'] = from_timestamp(data.get('forward_date'))
@ -369,6 +379,8 @@ class Message(TelegramObject):
data['photo'] = [p.to_dict() for p in self.photo]
if self.entities:
data['entities'] = [e.to_dict() for e in self.entities]
if self.caption_entities:
data['caption_entities'] = [e.to_dict() for e in self.caption_entities]
if self.new_chat_photo:
data['new_chat_photo'] = [p.to_dict() for p in self.new_chat_photo]
data['new_chat_member'] = data.pop('_new_chat_member', None)
@ -683,7 +695,7 @@ class Message(TelegramObject):
be an entity that belongs to this message.
Returns:
str: The text of the given entity
:obj:`str`: The text of the given entity
"""
# Is it a narrow build, if so we don't need to convert
@ -695,6 +707,31 @@ class Message(TelegramObject):
return entity_text.decode('utf-16-le')
def parse_caption_entity(self, entity):
"""Returns the text from a given :class:`telegram.MessageEntity`.
Note:
This method is present because Telegram calculates the offset and length in
UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
(That is, you can't just slice ``Message.caption`` with the offset and length.)
Args:
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
be an entity that belongs to this message.
Returns:
:obj:`str`: The text of the given entity
"""
# Is it a narrow build, if so we don't need to convert
if sys.maxunicode == 0xffff:
return self.caption[entity.offset:entity.offset + entity.length]
else:
entity_text = self.caption.encode('utf-16-le')
entity_text = entity_text[entity.offset * 2:(entity.offset + entity.length) * 2]
return entity_text.decode('utf-16-le')
def parse_entities(self, types=None):
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
@ -726,6 +763,37 @@ class Message(TelegramObject):
for entity in self.entities if entity.type in types
}
def parse_caption_entities(self, types=None):
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this message's caption filtered by their
:attr:`telegram.MessageEntity.type` attribute as the key, and the text that each entity
belongs to as the value of the :obj:`dict`.
Note:
This method should always be used instead of the :attr:`caption_entities` attribute,
since it calculates the correct substring from the message text based on UTF-16
codepoints. See :attr:`parse_entity` for more info.
Args:
types (List[:obj:`str`], optional): List of :class:`telegram.MessageEntity` types as
strings. If the ``type`` attribute of an entity is contained in this list, it will
be returned. Defaults to a list of all types. All types can be found as constants
in :class:`telegram.MessageEntity`.
Returns:
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
if types is None:
types = MessageEntity.ALL_TYPES
return {
entity: self.parse_caption_entity(entity)
for entity in self.caption_entities if entity.type in types
}
def _text_html(self, urled=False):
entities = self.parse_entities()
message_text = self.text

View file

@ -107,15 +107,6 @@ class TestBot(object):
# send_photo, send_audio, send_document, send_sticker, send_video, send_voice
# and send_video_note are tested in their respective test modules. No need to duplicate here.
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_location(self, bot, chat_id):
message = bot.send_location(chat_id=chat_id, latitude=-23.691288, longitude=-46.788279)
assert message.location
assert message.location.longitude == -46.788279
assert message.location.latitude == -23.691288
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_venue(self, bot, chat_id):
@ -368,6 +359,14 @@ class TestBot(object):
assert chat_member.status == 'administrator'
assert chat_member.user.username == 'EchteEldin'
@pytest.mark.skip(reason="Not implemented yet.")
def test_set_chat_sticker_set(self):
pass
@pytest.mark.skip(reason="Not implemented yet.")
def test_delete_chat_sticker_set(self):
pass
@pytest.mark.skipif(os.getenv('APPVEYOR'), reason='No game made for Appveyor bot (yet)')
@flaky(3, 1)
@pytest.mark.timeout(10)

View file

@ -27,7 +27,8 @@ from telegram import User
def chat(bot):
return Chat(TestChat.id, TestChat.title, TestChat.type,
all_members_are_administrators=TestChat.all_members_are_administrators,
bot=bot)
bot=bot, sticker_set_name=TestChat.sticker_set_name,
can_set_sticker_set=TestChat.can_set_sticker_set)
class TestChat(object):
@ -35,13 +36,17 @@ class TestChat(object):
title = 'ToledosPalaceBot - Group'
type = 'group'
all_members_are_administrators = False
sticker_set_name = 'stickers'
can_set_sticker_set = False
def test_de_json(self, bot):
json_dict = {
'id': TestChat.id,
'title': TestChat.title,
'type': TestChat.type,
'all_members_are_administrators': TestChat.all_members_are_administrators
'id': self.id,
'title': self.title,
'type': self.type,
'all_members_are_administrators': self.all_members_are_administrators,
'sticker_set_name': self.sticker_set_name,
'can_set_sticker_set': self.can_set_sticker_set
}
chat = Chat.de_json(json_dict, bot)
@ -49,6 +54,8 @@ class TestChat(object):
assert chat.title == self.title
assert chat.type == self.type
assert chat.all_members_are_administrators == self.all_members_are_administrators
assert chat.sticker_set_name == self.sticker_set_name
assert chat.can_set_sticker_set == self.can_set_sticker_set
def test_to_dict(self, chat):
chat_dict = chat.to_dict()

View file

@ -29,6 +29,7 @@ def inline_query_result_location():
TestInlineQueryResultLocation.latitude,
TestInlineQueryResultLocation.longitude,
TestInlineQueryResultLocation.title,
live_period=TestInlineQueryResultLocation.live_period,
thumb_url=TestInlineQueryResultLocation.thumb_url,
thumb_width=TestInlineQueryResultLocation.thumb_width,
thumb_height=TestInlineQueryResultLocation.thumb_height,
@ -42,6 +43,7 @@ class TestInlineQueryResultLocation(object):
latitude = 0.0
longitude = 1.0
title = 'title'
live_period = 70
thumb_url = 'thumb url'
thumb_width = 10
thumb_height = 15
@ -54,6 +56,7 @@ class TestInlineQueryResultLocation(object):
assert inline_query_result_location.latitude == self.latitude
assert inline_query_result_location.longitude == self.longitude
assert inline_query_result_location.title == self.title
assert inline_query_result_location.live_period == self.live_period
assert inline_query_result_location.thumb_url == self.thumb_url
assert inline_query_result_location.thumb_width == self.thumb_width
assert inline_query_result_location.thumb_height == self.thumb_height
@ -72,6 +75,8 @@ class TestInlineQueryResultLocation(object):
assert inline_query_result_location_dict['longitude'] == \
inline_query_result_location.longitude
assert inline_query_result_location_dict['title'] == inline_query_result_location.title
assert inline_query_result_location_dict[
'live_period'] == inline_query_result_location.live_period
assert inline_query_result_location_dict['thumb_url'] == \
inline_query_result_location.thumb_url
assert inline_query_result_location_dict['thumb_width'] == \

View file

@ -25,16 +25,19 @@ from telegram import InputLocationMessageContent
@pytest.fixture(scope='class')
def input_location_message_content():
return InputLocationMessageContent(TestInputLocationMessageContent.latitude,
TestInputLocationMessageContent.longitude)
TestInputLocationMessageContent.longitude,
live_period=TestInputLocationMessageContent.live_period)
class TestInputLocationMessageContent(object):
latitude = -23.691288
longitude = -46.788279
live_period = 80
def test_expected_values(self, input_location_message_content):
assert input_location_message_content.longitude == self.longitude
assert input_location_message_content.latitude == self.latitude
assert input_location_message_content.live_period == self.live_period
def test_to_dict(self, input_location_message_content):
input_location_message_content_dict = input_location_message_content.to_dict()
@ -44,3 +47,5 @@ class TestInputLocationMessageContent(object):
input_location_message_content.latitude
assert input_location_message_content_dict['longitude'] == \
input_location_message_content.longitude
assert input_location_message_content_dict[
'live_period'] == input_location_message_content.live_period

View file

@ -18,8 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
import pytest
from flaky import flaky
from telegram import Location
from telegram.error import BadRequest
@pytest.fixture(scope='class')
@ -39,6 +41,46 @@ class TestLocation(object):
assert location.latitude == self.latitude
assert location.longitude == self.longitude
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_live_location(self, bot, chat_id):
message = bot.send_location(chat_id=chat_id, latitude=52.223880, longitude=5.166146,
live_period=80)
assert message.location
assert message.location.latitude == 52.223880
assert message.location.longitude == 5.166146
message2 = bot.edit_message_live_location(message.chat_id, message.message_id,
latitude=52.223098, longitude=5.164306)
assert message2.location.latitude == 52.223098
assert message2.location.longitude == 5.164306
bot.stop_message_live_location(message.chat_id, message.message_id)
with pytest.raises(BadRequest, match="Message can't be edited"):
bot.edit_message_live_location(message.chat_id, message.message_id, latitude=52.223880,
longitude=5.164306)
# TODO: Needs improvement with in inline sent live location.
def test_edit_live_inline_message(self, monkeypatch, bot, location):
def test(_, url, data, **kwargs):
lat = data['latitude'] == location.latitude
lon = data['longitude'] == location.longitude
id = data['inline_message_id'] == 1234
return lat and lon and id
monkeypatch.setattr('telegram.utils.request.Request.post', test)
assert bot.edit_message_live_location(inline_message_id=1234, location=location)
# TODO: Needs improvement with in inline sent live location.
def test_stop_live_inline_message(self, monkeypatch, bot):
def test(_, url, data, **kwargs):
id = data['inline_message_id'] == 1234
return id
monkeypatch.setattr('telegram.utils.request.Request.post', test)
assert bot.stop_message_live_location(inline_message_id=1234)
def test_send_with_location(self, monkeypatch, bot, chat_id, location):
def test(_, url, data, **kwargs):
lat = data['latitude'] == location.latitude
@ -48,10 +90,32 @@ class TestLocation(object):
monkeypatch.setattr('telegram.utils.request.Request.post', test)
assert bot.send_location(location=location, chat_id=chat_id)
def test_edit_live_location_with_location(self, monkeypatch, bot, location):
def test(_, url, data, **kwargs):
lat = data['latitude'] == location.latitude
lon = data['longitude'] == location.longitude
return lat and lon
monkeypatch.setattr('telegram.utils.request.Request.post', test)
assert bot.edit_message_live_location(None, None, location=location)
def test_send_location_without_required(self, bot, chat_id):
with pytest.raises(ValueError, match='Either location or latitude and longitude'):
bot.send_location(chat_id=chat_id)
def test_edit_location_without_required(self, bot):
with pytest.raises(ValueError, match='Either location or latitude and longitude'):
bot.edit_message_live_location(chat_id=2, message_id=3)
def test_send_location_with_all_args(self, bot, location):
with pytest.raises(ValueError, match='Not both'):
bot.send_location(chat_id=1, latitude=2.5, longitude=4.6, location=location)
def test_edit_location_with_all_args(self, bot, location):
with pytest.raises(ValueError, match='Not both'):
bot.edit_message_live_location(chat_id=1, message_id=7, latitude=2.5, longitude=4.6,
location=location)
def test_to_dict(self, location):
location_dict = location.to_dict()

View file

@ -40,9 +40,12 @@ def message(bot):
'forward_date': datetime.now()},
{'reply_to_message': Message(50, None, None, None)},
{'edit_date': datetime.now()},
{'test': 'a text message',
{'text': 'a text message',
'enitites': [MessageEntity('bold', 10, 4),
MessageEntity('italic', 16, 7)]},
{'caption': 'A message caption',
'caption_entities': [MessageEntity('bold', 1, 1),
MessageEntity('text_link', 4, 3)]},
{'audio': Audio('audio_id', 12),
'caption': 'audio_file'},
{'document': Document('document_id'),
@ -78,12 +81,13 @@ def message(bot):
{'forward_signature': 'some_forward_sign'},
{'author_signature': 'some_author_sign'}
],
ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text', 'audio',
'document', 'game', 'photo', 'sticker', 'video', 'voice', 'video_note',
'new_members', 'contact', 'location', 'venue', 'left_member', 'new_title',
'new_photo', 'delete_photo', 'group_created', 'supergroup_created',
'channel_created', 'migrated_to', 'migrated_from', 'pinned', 'invoice',
'successful_payment', 'forward_signature', 'author_signature'])
ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text',
'caption_entities', 'audio', 'document', 'game', 'photo', 'sticker', 'video',
'voice', 'video_note', 'new_members', 'contact', 'location', 'venue',
'left_member', 'new_title', 'new_photo', 'delete_photo', 'group_created',
'supergroup_created', 'channel_created', 'migrated_to', 'migrated_from',
'pinned', 'invoice', 'successful_payment', 'forward_signature',
'author_signature'])
def message_params(bot, request):
return Message(message_id=TestMessage.id,
from_user=TestMessage.from_user,
@ -127,6 +131,14 @@ class TestMessage(object):
message = Message(1, self.from_user, self.date, self.chat, text=text, entities=[entity])
assert message.parse_entity(entity) == 'http://google.com'
def test_parse_caption_entity(self):
caption = (b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467'
b'\\u200d\\U0001f467\\U0001f431http://google.com').decode('unicode-escape')
entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17)
message = Message(1, self.from_user, self.date, self.chat, caption=caption,
caption_entities=[entity])
assert message.parse_caption_entity(entity) == 'http://google.com'
def test_parse_entities(self):
text = (b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467'
b'\\u200d\\U0001f467\\U0001f431http://google.com').decode('unicode-escape')
@ -137,6 +149,16 @@ class TestMessage(object):
assert message.parse_entities(MessageEntity.URL) == {entity: 'http://google.com'}
assert message.parse_entities() == {entity: 'http://google.com', entity_2: 'h'}
def test_parse_caption_entities(self):
text = (b'\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467'
b'\\u200d\\U0001f467\\U0001f431http://google.com').decode('unicode-escape')
entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17)
entity_2 = MessageEntity(type=MessageEntity.BOLD, offset=13, length=1)
message = Message(1, self.from_user, self.date, self.chat,
caption=text, caption_entities=[entity_2, entity])
assert message.parse_caption_entities(MessageEntity.URL) == {entity: 'http://google.com'}
assert message.parse_caption_entities() == {entity: 'http://google.com', entity_2: 'h'}
def test_text_html_simple(self):
test_html_string = ('Test for &lt;<b>bold</b>, <i>ita_lic</i>, <code>code</code>, '
'<a href="http://github.com/">links</a> and <pre>pre</pre>. '
@ -201,7 +223,6 @@ class TestMessage(object):
item = None
assert message_params.effective_attachment == item
def test_reply_text(self, monkeypatch, message):
def test(*args, **kwargs):
id = args[1] == message.chat_id

View file

@ -33,6 +33,8 @@ import telegram
IGNORED_OBJECTS = ('ResponseParameters', 'CallbackGame')
IGNORED_PARAMETERS = {'self', 'args', 'kwargs', 'read_latency', 'network_delay', 'timeout', 'bot',
'new_chat_member'}
# TODO: New_chat_member is still in our lib but already removed from TG's docs.
@ -49,10 +51,10 @@ def parse_table(h4):
if not table:
return []
head = [td.text for td in table.tr.find_all('td')]
row = namedtuple('{}TableRow'.format(h4.text), ','.join(head))
# row = namedtuple('{}TableRow'.format(h4.text), ','.join(head))
t = []
for tr in table.find_all('tr')[1:]:
t.append(row(*[td.text for td in tr.find_all('td')]))
t.append([td.text for td in tr.find_all('td')])
return t
@ -66,12 +68,12 @@ def check_method(h4):
checked = []
for parameter in table:
param = sig.parameters.get(parameter.Parameters)
assert param is not None, "Parameter {} not found in {}".format(parameter.Parameters,
param = sig.parameters.get(parameter[0])
assert param is not None, "Parameter {} not found in {}".format(parameter[0],
method.__name__)
# TODO: Check type via docstring
# TODO: Check if optional or required
checked.append(parameter.Parameters)
checked.append(parameter[0])
ignored = IGNORED_PARAMETERS.copy()
if name == 'getUpdates':
@ -82,7 +84,7 @@ def check_method(h4):
ignored |= {'edit_message'} # TODO: Now deprecated, so no longer in telegrams docs
elif name == 'sendContact':
ignored |= {'contact'} # Added for ease of use
elif name == 'sendLocation':
elif name in ['sendLocation', 'editMessageLiveLocation']:
ignored |= {'location'} # Added for ease of use
elif name == 'sendVenue':
ignored |= {'venue'} # Added for ease of use
@ -100,7 +102,7 @@ def check_object(h4):
checked = []
for parameter in table:
field = parameter.Field
field = parameter[0]
if field == 'from':
field = 'from_user'
elif name.startswith('InlineQueryResult') and field == 'type':