Introduce Builder Pattern for Updater and Dispatcher (#2646)

This commit is contained in:
Bibo-Joshi 2021-10-09 13:56:50 +02:00 committed by Hinrich Mahler
parent edb30cf342
commit 7afce46d9f
45 changed files with 2264 additions and 678 deletions

View file

@ -14,7 +14,7 @@ repos:
hooks:
- id: flake8
- repo: https://github.com/PyCQA/pylint
rev: v2.8.3
rev: v2.10.2
hooks:
- id: pylint
files: ^(telegram|examples)/.*\.py$
@ -27,12 +27,17 @@ repos:
- cachetools==4.2.2
- . # this basically does `pip install -e .`
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.812
rev: v0.910
hooks:
- id: mypy
name: mypy-ptb
files: ^telegram/.*\.py$
additional_dependencies:
- types-ujson
- types-pytz
- types-cryptography
- types-certifi
- types-cachetools
- certifi
- tornado>=6.1
- APScheduler==3.6.3
@ -51,7 +56,7 @@ repos:
- cachetools==4.2.2
- . # this basically does `pip install -e .`
- repo: https://github.com/asottile/pyupgrade
rev: v2.19.1
rev: v2.24.0
hooks:
- id: pyupgrade
files: ^(telegram|examples|tests)/.*\.py$

View file

@ -0,0 +1,7 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/builders.py
telegram.ext.DispatcherBuilder
==============================
.. autoclass:: telegram.ext.DispatcherBuilder
:members:

View file

@ -4,7 +4,9 @@ telegram.ext package
.. toctree::
telegram.ext.extbot
telegram.ext.updaterbuilder
telegram.ext.updater
telegram.ext.dispatcherbuilder
telegram.ext.dispatcher
telegram.ext.dispatcherhandlerstop
telegram.ext.callbackcontext
@ -61,4 +63,5 @@ utils
.. toctree::
telegram.ext.utils.promise
telegram.ext.utils.stack
telegram.ext.utils.types

View file

@ -0,0 +1,7 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/builders.py
telegram.ext.UpdaterBuilder
===========================
.. autoclass:: telegram.ext.UpdaterBuilder
:members:

View file

@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/utils/stack.py
telegram.ext.utils.stack Module
================================
.. automodule:: telegram.ext.utils.stack
:members:
:show-inheritance:

View file

@ -11,27 +11,29 @@ from typing import List, Tuple, cast
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (
Updater,
CommandHandler,
CallbackQueryHandler,
CallbackContext,
InvalidCallbackData,
PicklePersistence,
Updater,
CallbackContext,
)
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
def start(update: Update, context: CallbackContext) -> None:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Sends a message with 5 inline buttons attached."""
number_list: List[int] = []
update.message.reply_text('Please choose:', reply_markup=build_keyboard(number_list))
def help_command(update: Update, context: CallbackContext) -> None:
def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Displays info on how to use the bot."""
update.message.reply_text(
"Use /start to test this bot. Use /clear to clear the stored data so that you can see "
@ -39,10 +41,10 @@ def help_command(update: Update, context: CallbackContext) -> None:
)
def clear(update: Update, context: CallbackContext) -> None:
def clear(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Clears the callback data cache"""
context.bot.callback_data_cache.clear_callback_data() # type: ignore[attr-defined]
context.bot.callback_data_cache.clear_callback_queries() # type: ignore[attr-defined]
context.bot.callback_data_cache.clear_callback_data()
context.bot.callback_data_cache.clear_callback_queries()
update.effective_message.reply_text('All clear!')
@ -53,7 +55,7 @@ def build_keyboard(current_list: List[int]) -> InlineKeyboardMarkup:
)
def list_button(update: Update, context: CallbackContext) -> None:
def list_button(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Parses the CallbackQuery and updates the message text."""
query = update.callback_query
query.answer()
@ -73,7 +75,7 @@ def list_button(update: Update, context: CallbackContext) -> None:
context.drop_callback_data(query)
def handle_invalid_button(update: Update, context: CallbackContext) -> None:
def handle_invalid_button(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Informs the user that the button is no longer available."""
update.callback_query.answer()
update.effective_message.edit_text(
@ -86,7 +88,13 @@ def main() -> None:
# We use persistence to demonstrate how buttons can still work after the bot was restarted
persistence = PicklePersistence(filepath='arbitrarycallbackdatabot')
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN", persistence=persistence, arbitrary_callback_data=True)
updater = (
Updater.builder()
.token("TOKEN")
.persistence(persistence)
.arbitrary_callback_data(True)
.build()
)
updater.dispatcher.add_handler(CommandHandler('start', start))
updater.dispatcher.add_handler(CommandHandler('help', help_command))

View file

@ -16,13 +16,14 @@ from typing import Tuple, Optional
from telegram import Update, Chat, ChatMember, ParseMode, ChatMemberUpdated
from telegram.ext import (
Updater,
CommandHandler,
CallbackContext,
ChatMemberHandler,
Updater,
CallbackContext,
)
# Enable logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
@ -66,7 +67,7 @@ def extract_status_change(
return was_member, is_member
def track_chats(update: Update, context: CallbackContext) -> None:
def track_chats(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Tracks the chats the bot is in."""
result = extract_status_change(update.my_chat_member)
if result is None:
@ -101,7 +102,7 @@ def track_chats(update: Update, context: CallbackContext) -> None:
context.bot_data.setdefault("channel_ids", set()).discard(chat.id)
def show_chats(update: Update, context: CallbackContext) -> None:
def show_chats(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Shows which chats the bot is in"""
user_ids = ", ".join(str(uid) for uid in context.bot_data.setdefault("user_ids", set()))
group_ids = ", ".join(str(gid) for gid in context.bot_data.setdefault("group_ids", set()))
@ -114,7 +115,7 @@ def show_chats(update: Update, context: CallbackContext) -> None:
update.effective_message.reply_text(text)
def greet_chat_members(update: Update, context: CallbackContext) -> None:
def greet_chat_members(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Greets new users in chats and announces when someone leaves"""
result = extract_status_change(update.chat_member)
if result is None:
@ -139,7 +140,7 @@ def greet_chat_members(update: Update, context: CallbackContext) -> None:
def main() -> None:
"""Start the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -15,13 +15,14 @@ from typing import DefaultDict, Optional, Set
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ParseMode
from telegram.ext import (
Updater,
CommandHandler,
CallbackContext,
ContextTypes,
CallbackQueryHandler,
TypeHandler,
Dispatcher,
ExtBot,
Updater,
)
@ -32,8 +33,8 @@ class ChatData:
self.clicks_per_message: DefaultDict[int, int] = defaultdict(int)
# The [dict, ChatData, dict] is for type checkers like mypy
class CustomContext(CallbackContext[dict, ChatData, dict]):
# The [ExtBot, dict, ChatData, dict] is for type checkers like mypy
class CustomContext(CallbackContext[ExtBot, dict, ChatData, dict]):
"""Custom class for context."""
def __init__(self, dispatcher: Dispatcher):
@ -113,7 +114,7 @@ def track_users(update: Update, context: CustomContext) -> None:
def main() -> None:
"""Run the bot."""
context_types = ContextTypes(context=CustomContext, chat_data=ChatData)
updater = Updater("TOKEN", context_types=context_types)
updater = Updater.builder().token("TOKEN").context_types(context_types).build()
dispatcher = updater.dispatcher
# run track_users in its own group to not interfere with the user handlers

View file

@ -18,25 +18,25 @@ import logging
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
from telegram.ext import (
Updater,
CommandHandler,
MessageHandler,
Filters,
ConversationHandler,
Updater,
CallbackContext,
)
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
GENDER, PHOTO, LOCATION, BIO = range(4)
def start(update: Update, context: CallbackContext) -> int:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Starts the conversation and asks the user about their gender."""
reply_keyboard = [['Boy', 'Girl', 'Other']]
@ -52,7 +52,7 @@ def start(update: Update, context: CallbackContext) -> int:
return GENDER
def gender(update: Update, context: CallbackContext) -> int:
def gender(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Stores the selected gender and asks for a photo."""
user = update.message.from_user
logger.info("Gender of %s: %s", user.first_name, update.message.text)
@ -65,7 +65,7 @@ def gender(update: Update, context: CallbackContext) -> int:
return PHOTO
def photo(update: Update, context: CallbackContext) -> int:
def photo(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Stores the photo and asks for a location."""
user = update.message.from_user
photo_file = update.message.photo[-1].get_file()
@ -78,7 +78,7 @@ def photo(update: Update, context: CallbackContext) -> int:
return LOCATION
def skip_photo(update: Update, context: CallbackContext) -> int:
def skip_photo(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Skips the photo and asks for a location."""
user = update.message.from_user
logger.info("User %s did not send a photo.", user.first_name)
@ -89,7 +89,7 @@ def skip_photo(update: Update, context: CallbackContext) -> int:
return LOCATION
def location(update: Update, context: CallbackContext) -> int:
def location(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Stores the location and asks for some info about the user."""
user = update.message.from_user
user_location = update.message.location
@ -103,7 +103,7 @@ def location(update: Update, context: CallbackContext) -> int:
return BIO
def skip_location(update: Update, context: CallbackContext) -> int:
def skip_location(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Skips the location and asks for info about the user."""
user = update.message.from_user
logger.info("User %s did not send a location.", user.first_name)
@ -114,7 +114,7 @@ def skip_location(update: Update, context: CallbackContext) -> int:
return BIO
def bio(update: Update, context: CallbackContext) -> int:
def bio(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Stores the info about the user and ends the conversation."""
user = update.message.from_user
logger.info("Bio of %s: %s", user.first_name, update.message.text)
@ -123,7 +123,7 @@ def bio(update: Update, context: CallbackContext) -> int:
return ConversationHandler.END
def cancel(update: Update, context: CallbackContext) -> int:
def cancel(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Cancels and ends the conversation."""
user = update.message.from_user
logger.info("User %s canceled the conversation.", user.first_name)
@ -137,7 +137,7 @@ def cancel(update: Update, context: CallbackContext) -> int:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -19,19 +19,19 @@ from typing import Dict
from telegram import ReplyKeyboardMarkup, Update, ReplyKeyboardRemove
from telegram.ext import (
Updater,
CommandHandler,
MessageHandler,
Filters,
ConversationHandler,
Updater,
CallbackContext,
)
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
CHOOSING, TYPING_REPLY, TYPING_CHOICE = range(3)
@ -50,7 +50,7 @@ def facts_to_str(user_data: Dict[str, str]) -> str:
return "\n".join(facts).join(['\n', '\n'])
def start(update: Update, context: CallbackContext) -> int:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Start the conversation and ask user for input."""
update.message.reply_text(
"Hi! My name is Doctor Botter. I will hold a more complex conversation with you. "
@ -61,7 +61,7 @@ def start(update: Update, context: CallbackContext) -> int:
return CHOOSING
def regular_choice(update: Update, context: CallbackContext) -> int:
def regular_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Ask the user for info about the selected predefined choice."""
text = update.message.text
context.user_data['choice'] = text
@ -70,7 +70,7 @@ def regular_choice(update: Update, context: CallbackContext) -> int:
return TYPING_REPLY
def custom_choice(update: Update, context: CallbackContext) -> int:
def custom_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Ask the user for a description of a custom category."""
update.message.reply_text(
'Alright, please send me the category first, for example "Most impressive skill"'
@ -79,7 +79,7 @@ def custom_choice(update: Update, context: CallbackContext) -> int:
return TYPING_CHOICE
def received_information(update: Update, context: CallbackContext) -> int:
def received_information(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Store info provided by user and ask for the next category."""
user_data = context.user_data
text = update.message.text
@ -97,7 +97,7 @@ def received_information(update: Update, context: CallbackContext) -> int:
return CHOOSING
def done(update: Update, context: CallbackContext) -> int:
def done(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Display the gathered info and end the conversation."""
user_data = context.user_data
if 'choice' in user_data:
@ -115,7 +115,7 @@ def done(update: Update, context: CallbackContext) -> int:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -22,10 +22,10 @@ import logging
from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton, Update, helpers
from telegram.ext import (
Updater,
CommandHandler,
CallbackQueryHandler,
Filters,
Updater,
CallbackContext,
)
@ -46,7 +46,7 @@ SO_COOL = "so-cool"
KEYBOARD_CALLBACKDATA = "keyboard-callback-data"
def start(update: Update, context: CallbackContext) -> None:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Send a deep-linked URL when the command /start is issued."""
bot = context.bot
url = helpers.create_deep_linked_url(bot.username, CHECK_THIS_OUT, group=True)
@ -54,7 +54,7 @@ def start(update: Update, context: CallbackContext) -> None:
update.message.reply_text(text)
def deep_linked_level_1(update: Update, context: CallbackContext) -> None:
def deep_linked_level_1(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Reached through the CHECK_THIS_OUT payload"""
bot = context.bot
url = helpers.create_deep_linked_url(bot.username, SO_COOL)
@ -68,7 +68,7 @@ def deep_linked_level_1(update: Update, context: CallbackContext) -> None:
update.message.reply_text(text, reply_markup=keyboard)
def deep_linked_level_2(update: Update, context: CallbackContext) -> None:
def deep_linked_level_2(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Reached through the SO_COOL payload"""
bot = context.bot
url = helpers.create_deep_linked_url(bot.username, USING_ENTITIES)
@ -76,7 +76,7 @@ def deep_linked_level_2(update: Update, context: CallbackContext) -> None:
update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True)
def deep_linked_level_3(update: Update, context: CallbackContext) -> None:
def deep_linked_level_3(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Reached through the USING_ENTITIES payload"""
update.message.reply_text(
"It is also possible to make deep-linking using InlineKeyboardButtons.",
@ -86,14 +86,14 @@ def deep_linked_level_3(update: Update, context: CallbackContext) -> None:
)
def deep_link_level_3_callback(update: Update, context: CallbackContext) -> None:
def deep_link_level_3_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Answers CallbackQuery with deeplinking url."""
bot = context.bot
url = helpers.create_deep_linked_url(bot.username, USING_KEYBOARD)
update.callback_query.answer(url=url)
def deep_linked_level_4(update: Update, context: CallbackContext) -> None:
def deep_linked_level_4(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Reached through the USING_KEYBOARD payload"""
payload = context.args
update.message.reply_text(
@ -104,7 +104,7 @@ def deep_linked_level_4(update: Update, context: CallbackContext) -> None:
def main() -> None:
"""Start the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -18,19 +18,25 @@ bot.
import logging
from telegram import Update, ForceReply
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackContext
from telegram.ext import (
CommandHandler,
MessageHandler,
Filters,
Updater,
CallbackContext,
)
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments update and
# context.
def start(update: Update, context: CallbackContext) -> None:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Send a message when the command /start is issued."""
user = update.effective_user
update.message.reply_markdown_v2(
@ -39,12 +45,12 @@ def start(update: Update, context: CallbackContext) -> None:
)
def help_command(update: Update, context: CallbackContext) -> None:
def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Send a message when the command /help is issued."""
update.message.reply_text('Help!')
def echo(update: Update, context: CallbackContext) -> None:
def echo(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Echo the user message."""
update.message.reply_text(update.message.text)
@ -52,7 +58,7 @@ def echo(update: Update, context: CallbackContext) -> None:
def main() -> None:
"""Start the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -9,12 +9,12 @@ import logging
import traceback
from telegram import Update, ParseMode
from telegram.ext import Updater, CallbackContext, CommandHandler
from telegram.ext import CommandHandler, Updater, CallbackContext
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
# The token you got from @botfather when you created the bot
@ -25,7 +25,7 @@ BOT_TOKEN = "TOKEN"
DEVELOPER_CHAT_ID = 123456789
def error_handler(update: object, context: CallbackContext) -> None:
def error_handler(update: object, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Log the error and send a telegram message to notify the developer."""
# Log the error before we do anything else, so we can see it even if something breaks.
logger.error(msg="Exception while handling an update:", exc_info=context.error)
@ -51,12 +51,12 @@ def error_handler(update: object, context: CallbackContext) -> None:
context.bot.send_message(chat_id=DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML)
def bad_command(update: Update, context: CallbackContext) -> None:
def bad_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Raise an error to trigger the error handler."""
context.bot.wrong_method_name() # type: ignore[attr-defined]
def start(update: Update, context: CallbackContext) -> None:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Displays info on how to trigger an error."""
update.effective_message.reply_html(
'Use /bad_command to cause an error.\n'
@ -67,7 +67,7 @@ def start(update: Update, context: CallbackContext) -> None:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater(BOT_TOKEN)
updater = Updater.builder().token(BOT_TOKEN).build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -23,23 +23,22 @@ from telegram.ext import Updater, InlineQueryHandler, CommandHandler, CallbackCo
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments update and
# context. Error handlers also receive the raised TelegramError object in error.
def start(update: Update, context: CallbackContext) -> None:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Send a message when the command /start is issued."""
update.message.reply_text('Hi!')
def help_command(update: Update, context: CallbackContext) -> None:
def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Send a message when the command /help is issued."""
update.message.reply_text('Help!')
def inlinequery(update: Update, context: CallbackContext) -> None:
def inlinequery(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Handle the inline query."""
query = update.inline_query.query
@ -74,7 +73,7 @@ def inlinequery(update: Update, context: CallbackContext) -> None:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -9,15 +9,22 @@ Basic example for a bot that uses inline keyboards. For an in-depth explanation,
import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, CallbackContext
from telegram.ext import (
CommandHandler,
CallbackQueryHandler,
Updater,
CallbackContext,
)
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
def start(update: Update, context: CallbackContext) -> None:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Sends a message with three inline buttons attached."""
keyboard = [
[
@ -32,7 +39,7 @@ def start(update: Update, context: CallbackContext) -> None:
update.message.reply_text('Please choose:', reply_markup=reply_markup)
def button(update: Update, context: CallbackContext) -> None:
def button(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Parses the CallbackQuery and updates the message text."""
query = update.callback_query
@ -43,7 +50,7 @@ def button(update: Update, context: CallbackContext) -> None:
query.edit_message_text(text=f"Selected option: {query.data}")
def help_command(update: Update, context: CallbackContext) -> None:
def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Displays info on how to use the bot."""
update.message.reply_text("Use /start to test this bot.")
@ -51,7 +58,7 @@ def help_command(update: Update, context: CallbackContext) -> None:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
updater.dispatcher.add_handler(CommandHandler('start', start))
updater.dispatcher.add_handler(CallbackQueryHandler(button))

View file

@ -17,18 +17,18 @@ Press Ctrl-C on the command line to stop the bot.
import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (
Updater,
CommandHandler,
CallbackQueryHandler,
ConversationHandler,
Updater,
CallbackContext,
)
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
# Stages
@ -37,7 +37,7 @@ FIRST, SECOND = range(2)
ONE, TWO, THREE, FOUR = range(4)
def start(update: Update, context: CallbackContext) -> int:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Send message on `/start`."""
# Get user that sent /start and log his name
user = update.message.from_user
@ -59,7 +59,7 @@ def start(update: Update, context: CallbackContext) -> int:
return FIRST
def start_over(update: Update, context: CallbackContext) -> int:
def start_over(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Prompt same text & keyboard as `start` does but not as new message"""
# Get CallbackQuery from Update
query = update.callback_query
@ -80,7 +80,7 @@ def start_over(update: Update, context: CallbackContext) -> int:
return FIRST
def one(update: Update, context: CallbackContext) -> int:
def one(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
@ -97,7 +97,7 @@ def one(update: Update, context: CallbackContext) -> int:
return FIRST
def two(update: Update, context: CallbackContext) -> int:
def two(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
@ -114,7 +114,7 @@ def two(update: Update, context: CallbackContext) -> int:
return FIRST
def three(update: Update, context: CallbackContext) -> int:
def three(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
@ -132,7 +132,7 @@ def three(update: Update, context: CallbackContext) -> int:
return SECOND
def four(update: Update, context: CallbackContext) -> int:
def four(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
@ -149,7 +149,7 @@ def four(update: Update, context: CallbackContext) -> int:
return FIRST
def end(update: Update, context: CallbackContext) -> int:
def end(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Returns `ConversationHandler.END`, which tells the
ConversationHandler that the conversation is over.
"""
@ -162,7 +162,7 @@ def end(update: Update, context: CallbackContext) -> int:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -19,20 +19,20 @@ from typing import Tuple, Dict, Any
from telegram import InlineKeyboardMarkup, InlineKeyboardButton, Update
from telegram.ext import (
Updater,
CommandHandler,
MessageHandler,
Filters,
ConversationHandler,
CallbackQueryHandler,
Updater,
CallbackContext,
)
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
# State definitions for top level conversation
@ -71,7 +71,7 @@ def _name_switcher(level: str) -> Tuple[str, str]:
# Top level conversation callbacks
def start(update: Update, context: CallbackContext) -> str:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
"""Select an action: Adding parent/child or show data."""
text = (
"You may choose to add a family member, yourself, show the gathered data, or end the "
@ -104,7 +104,7 @@ def start(update: Update, context: CallbackContext) -> str:
return SELECTING_ACTION
def adding_self(update: Update, context: CallbackContext) -> str:
def adding_self(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
"""Add information about yourself."""
context.user_data[CURRENT_LEVEL] = SELF
text = 'Okay, please tell me about yourself.'
@ -117,7 +117,7 @@ def adding_self(update: Update, context: CallbackContext) -> str:
return DESCRIBING_SELF
def show_data(update: Update, context: CallbackContext) -> str:
def show_data(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
"""Pretty print gathered data."""
def prettyprint(user_data: Dict[str, Any], level: str) -> str:
@ -152,14 +152,14 @@ def show_data(update: Update, context: CallbackContext) -> str:
return SHOWING
def stop(update: Update, context: CallbackContext) -> int:
def stop(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""End Conversation by command."""
update.message.reply_text('Okay, bye.')
return END
def end(update: Update, context: CallbackContext) -> int:
def end(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""End conversation from InlineKeyboardButton."""
update.callback_query.answer()
@ -170,7 +170,7 @@ def end(update: Update, context: CallbackContext) -> int:
# Second level conversation callbacks
def select_level(update: Update, context: CallbackContext) -> str:
def select_level(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
"""Choose to add a parent or a child."""
text = 'You may add a parent or a child. Also you can show the gathered data or go back.'
buttons = [
@ -191,7 +191,7 @@ def select_level(update: Update, context: CallbackContext) -> str:
return SELECTING_LEVEL
def select_gender(update: Update, context: CallbackContext) -> str:
def select_gender(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
"""Choose to add mother or father."""
level = update.callback_query.data
context.user_data[CURRENT_LEVEL] = level
@ -218,7 +218,7 @@ def select_gender(update: Update, context: CallbackContext) -> str:
return SELECTING_GENDER
def end_second_level(update: Update, context: CallbackContext) -> int:
def end_second_level(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Return to top level conversation."""
context.user_data[START_OVER] = True
start(update, context)
@ -227,7 +227,7 @@ def end_second_level(update: Update, context: CallbackContext) -> int:
# Third level callbacks
def select_feature(update: Update, context: CallbackContext) -> str:
def select_feature(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
"""Select a feature to update for the person."""
buttons = [
[
@ -254,7 +254,7 @@ def select_feature(update: Update, context: CallbackContext) -> str:
return SELECTING_FEATURE
def ask_for_input(update: Update, context: CallbackContext) -> str:
def ask_for_input(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
"""Prompt user to input data for selected feature."""
context.user_data[CURRENT_FEATURE] = update.callback_query.data
text = 'Okay, tell me.'
@ -265,7 +265,7 @@ def ask_for_input(update: Update, context: CallbackContext) -> str:
return TYPING
def save_input(update: Update, context: CallbackContext) -> str:
def save_input(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
"""Save input for feature and return to feature selection."""
user_data = context.user_data
user_data[FEATURES][user_data[CURRENT_FEATURE]] = update.message.text
@ -275,7 +275,7 @@ def save_input(update: Update, context: CallbackContext) -> str:
return select_feature(update, context)
def end_describing(update: Update, context: CallbackContext) -> int:
def end_describing(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""End gathering of features and return to parent conversation."""
user_data = context.user_data
level = user_data[CURRENT_LEVEL]
@ -293,7 +293,7 @@ def end_describing(update: Update, context: CallbackContext) -> int:
return END
def stop_nested(update: Update, context: CallbackContext) -> str:
def stop_nested(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
"""Completely end conversation from within nested conversation."""
update.message.reply_text('Okay, bye.')
@ -303,7 +303,7 @@ def stop_nested(update: Update, context: CallbackContext) -> str:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -14,9 +14,10 @@ import logging
from pathlib import Path
from telegram import Update
from telegram.ext import Updater, MessageHandler, Filters, CallbackContext
from telegram.ext import MessageHandler, Filters, Updater, CallbackContext
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG
)
@ -24,7 +25,7 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
def msg(update: Update, context: CallbackContext) -> None:
def msg(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Downloads and prints the received passport data."""
# Retrieve passport data
passport_data = update.message.passport_data
@ -102,7 +103,8 @@ def msg(update: Update, context: CallbackContext) -> None:
def main() -> None:
"""Start the bot."""
# Create the Updater and pass it your token and private key
updater = Updater("TOKEN", private_key=Path('private.key').read_bytes())
private_key = Path('private.key')
updater = Updater.builder().token("TOKEN").private_key(private_key.read_bytes()).build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -8,24 +8,24 @@ import logging
from telegram import LabeledPrice, ShippingOption, Update
from telegram.ext import (
Updater,
CommandHandler,
MessageHandler,
Filters,
PreCheckoutQueryHandler,
ShippingQueryHandler,
Updater,
CallbackContext,
)
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
def start_callback(update: Update, context: CallbackContext) -> None:
def start_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Displays info on how to use the bot."""
msg = (
"Use /shipping to get an invoice for shipping-payment, or /noshipping for an "
@ -35,7 +35,7 @@ def start_callback(update: Update, context: CallbackContext) -> None:
update.message.reply_text(msg)
def start_with_shipping_callback(update: Update, context: CallbackContext) -> None:
def start_with_shipping_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Sends an invoice with shipping-payment."""
chat_id = update.message.chat_id
title = "Payment Example"
@ -69,7 +69,7 @@ def start_with_shipping_callback(update: Update, context: CallbackContext) -> No
)
def start_without_shipping_callback(update: Update, context: CallbackContext) -> None:
def start_without_shipping_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Sends an invoice without shipping-payment."""
chat_id = update.message.chat_id
title = "Payment Example"
@ -91,7 +91,7 @@ def start_without_shipping_callback(update: Update, context: CallbackContext) ->
)
def shipping_callback(update: Update, context: CallbackContext) -> None:
def shipping_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Answers the ShippingQuery with ShippingOptions"""
query = update.shipping_query
# check the payload, is this from your bot?
@ -109,7 +109,7 @@ def shipping_callback(update: Update, context: CallbackContext) -> None:
# after (optional) shipping, it's the pre-checkout
def precheckout_callback(update: Update, context: CallbackContext) -> None:
def precheckout_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Answers the PreQecheckoutQuery"""
query = update.pre_checkout_query
# check the payload, is this from your bot?
@ -121,7 +121,7 @@ def precheckout_callback(update: Update, context: CallbackContext) -> None:
# finally, after contacting the payment provider...
def successful_payment_callback(update: Update, context: CallbackContext) -> None:
def successful_payment_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Confirms the successful payment."""
# do something after successfully receiving payment?
update.message.reply_text("Thank you for your payment!")
@ -130,7 +130,7 @@ def successful_payment_callback(update: Update, context: CallbackContext) -> Non
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -19,20 +19,20 @@ from typing import Dict
from telegram import ReplyKeyboardMarkup, Update, ReplyKeyboardRemove
from telegram.ext import (
Updater,
CommandHandler,
MessageHandler,
Filters,
ConversationHandler,
PicklePersistence,
Updater,
CallbackContext,
)
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
CHOOSING, TYPING_REPLY, TYPING_CHOICE = range(3)
@ -51,7 +51,7 @@ def facts_to_str(user_data: Dict[str, str]) -> str:
return "\n".join(facts).join(['\n', '\n'])
def start(update: Update, context: CallbackContext) -> int:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Start the conversation, display any stored data and ask user for input."""
reply_text = "Hi! My name is Doctor Botter."
if context.user_data:
@ -69,7 +69,7 @@ def start(update: Update, context: CallbackContext) -> int:
return CHOOSING
def regular_choice(update: Update, context: CallbackContext) -> int:
def regular_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Ask the user for info about the selected predefined choice."""
text = update.message.text.lower()
context.user_data['choice'] = text
@ -84,7 +84,7 @@ def regular_choice(update: Update, context: CallbackContext) -> int:
return TYPING_REPLY
def custom_choice(update: Update, context: CallbackContext) -> int:
def custom_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Ask the user for a description of a custom category."""
update.message.reply_text(
'Alright, please send me the category first, for example "Most impressive skill"'
@ -93,7 +93,7 @@ def custom_choice(update: Update, context: CallbackContext) -> int:
return TYPING_CHOICE
def received_information(update: Update, context: CallbackContext) -> int:
def received_information(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Store info provided by user and ask for the next category."""
text = update.message.text
category = context.user_data['choice']
@ -110,14 +110,14 @@ def received_information(update: Update, context: CallbackContext) -> int:
return CHOOSING
def show_data(update: Update, context: CallbackContext) -> None:
def show_data(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Display the gathered info."""
update.message.reply_text(
f"This is what you already told me: {facts_to_str(context.user_data)}"
)
def done(update: Update, context: CallbackContext) -> int:
def done(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Display the gathered info and end the conversation."""
if 'choice' in context.user_data:
del context.user_data['choice']
@ -133,7 +133,7 @@ def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
persistence = PicklePersistence(filepath='conversationbot')
updater = Updater("TOKEN", persistence=persistence)
updater = Updater.builder().token("TOKEN").persistence(persistence).build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -19,22 +19,24 @@ from telegram import (
Update,
)
from telegram.ext import (
Updater,
CommandHandler,
PollAnswerHandler,
PollHandler,
MessageHandler,
Filters,
Updater,
CallbackContext,
)
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
def start(update: Update, context: CallbackContext) -> None:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Inform user about what this bot can do"""
update.message.reply_text(
'Please select /poll to get a Poll, /quiz to get a Quiz or /preview'
@ -42,7 +44,7 @@ def start(update: Update, context: CallbackContext) -> None:
)
def poll(update: Update, context: CallbackContext) -> None:
def poll(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Sends a predefined poll"""
questions = ["Good", "Really good", "Fantastic", "Great"]
message = context.bot.send_poll(
@ -64,7 +66,7 @@ def poll(update: Update, context: CallbackContext) -> None:
context.bot_data.update(payload)
def receive_poll_answer(update: Update, context: CallbackContext) -> None:
def receive_poll_answer(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Summarize a users poll vote"""
answer = update.poll_answer
poll_id = answer.poll_id
@ -93,7 +95,7 @@ def receive_poll_answer(update: Update, context: CallbackContext) -> None:
)
def quiz(update: Update, context: CallbackContext) -> None:
def quiz(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Send a predefined poll"""
questions = ["1", "2", "4", "20"]
message = update.effective_message.reply_poll(
@ -106,7 +108,7 @@ def quiz(update: Update, context: CallbackContext) -> None:
context.bot_data.update(payload)
def receive_quiz_answer(update: Update, context: CallbackContext) -> None:
def receive_quiz_answer(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Close quiz after three participants took it"""
# the bot can receive closed poll updates we don't care about
if update.poll.is_closed:
@ -120,7 +122,7 @@ def receive_quiz_answer(update: Update, context: CallbackContext) -> None:
context.bot.stop_poll(quiz_data["chat_id"], quiz_data["message_id"])
def preview(update: Update, context: CallbackContext) -> None:
def preview(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Ask user to create a poll and display a preview of it"""
# using this without a type lets the user chooses what he wants (quiz or poll)
button = [[KeyboardButton("Press me!", request_poll=KeyboardButtonPollType())]]
@ -131,7 +133,7 @@ def preview(update: Update, context: CallbackContext) -> None:
)
def receive_poll(update: Update, context: CallbackContext) -> None:
def receive_poll(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""On receiving polls, reply to it by a closed poll copying the received poll"""
actual_poll = update.effective_message.poll
# Only need to set the question and options, since all other parameters don't matter for
@ -145,7 +147,7 @@ def receive_poll(update: Update, context: CallbackContext) -> None:
)
def help_handler(update: Update, context: CallbackContext) -> None:
def help_handler(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Display a help message"""
update.message.reply_text("Use /quiz, /poll or /preview to test this bot.")
@ -153,7 +155,7 @@ def help_handler(update: Update, context: CallbackContext) -> None:
def main() -> None:
"""Run bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
dispatcher = updater.dispatcher
dispatcher.add_handler(CommandHandler('start', start))
dispatcher.add_handler(CommandHandler('poll', poll))

View file

@ -21,13 +21,12 @@ bot.
import logging
from telegram import Update
from telegram.ext import Updater, CommandHandler, CallbackContext
from telegram.ext import CommandHandler, Updater, CallbackContext
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
@ -37,18 +36,18 @@ logger = logging.getLogger(__name__)
# since context is an unused local variable.
# This being an example and not having context present confusing beginners,
# we decided to have it present as context.
def start(update: Update, context: CallbackContext) -> None:
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Sends explanation on how to use the bot."""
update.message.reply_text('Hi! Use /set <seconds> to set a timer')
def alarm(context: CallbackContext) -> None:
def alarm(context: CallbackContext.DEFAULT_TYPE) -> None:
"""Send the alarm message."""
job = context.job
context.bot.send_message(job.context, text='Beep!')
def remove_job_if_exists(name: str, context: CallbackContext) -> bool:
def remove_job_if_exists(name: str, context: CallbackContext.DEFAULT_TYPE) -> bool:
"""Remove job with given name. Returns whether job was removed."""
current_jobs = context.job_queue.get_jobs_by_name(name)
if not current_jobs:
@ -58,7 +57,7 @@ def remove_job_if_exists(name: str, context: CallbackContext) -> bool:
return True
def set_timer(update: Update, context: CallbackContext) -> None:
def set_timer(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Add a job to the queue."""
chat_id = update.message.chat_id
try:
@ -80,7 +79,7 @@ def set_timer(update: Update, context: CallbackContext) -> None:
update.message.reply_text('Usage: /set <seconds>')
def unset(update: Update, context: CallbackContext) -> None:
def unset(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
"""Remove the job if the user changed their mind."""
chat_id = update.message.chat_id
job_removed = remove_job_if_exists(str(chat_id), context)
@ -91,7 +90,7 @@ def unset(update: Update, context: CallbackContext) -> None:
def main() -> None:
"""Run bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
updater = Updater.builder().token("TOKEN").build()
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher

View file

@ -5,9 +5,9 @@ pre-commit
# Make sure that the versions specified here match the pre-commit settings!
black==20.8b1
flake8==3.9.2
pylint==2.8.3
mypy==0.812
pyupgrade==2.19.1
pylint==2.10.2
mypy==0.910
pyupgrade==2.24.0
pytest==6.2.4

View file

@ -158,22 +158,16 @@ class Bot(TelegramObject):
def __init__(
self,
token: str,
base_url: str = None,
base_file_url: str = None,
base_url: str = 'https://api.telegram.org/bot',
base_file_url: str = 'https://api.telegram.org/file/bot',
request: 'Request' = None,
private_key: bytes = None,
private_key_password: bytes = None,
):
self.token = self._validate_token(token)
if base_url is None:
base_url = 'https://api.telegram.org/bot'
if base_file_url is None:
base_file_url = 'https://api.telegram.org/file/bot'
self.base_url = str(base_url) + str(self.token)
self.base_file_url = str(base_file_url) + str(self.token)
self.base_url = base_url + self.token
self.base_file_url = base_file_url + self.token
self._bot: Optional[User] = None
self._request = request or Request()
self.private_key = None
@ -2796,8 +2790,8 @@ class Bot(TelegramObject):
Telegram API.
Returns:
:class:`telegram.Message`: On success, if the edited message is not an inline message
, the edited Message is returned, otherwise :obj:`True` is returned.
:class:`telegram.Message`: On success, if edited message is not an inline message, the
edited Message is returned, otherwise :obj:`True` is returned.
Raises:
:class:`telegram.error.TelegramError`

View file

@ -47,6 +47,7 @@ from .chatmemberhandler import ChatMemberHandler
from .chatjoinrequesthandler import ChatJoinRequestHandler
from .defaults import Defaults
from .callbackdatacache import CallbackDataCache, InvalidCallbackData
from .builders import DispatcherBuilder, UpdaterBuilder
__all__ = (
'BaseFilter',
@ -63,6 +64,7 @@ __all__ = (
'Defaults',
'DictPersistence',
'Dispatcher',
'DispatcherBuilder',
'DispatcherHandlerStop',
'ExtBot',
'Filters',
@ -85,4 +87,5 @@ __all__ = (
'TypeHandler',
'UpdateFilter',
'Updater',
'UpdaterBuilder',
)

1206
telegram/ext/builders.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -34,15 +34,14 @@ from typing import (
from telegram import Update, CallbackQuery
from telegram.ext import ExtBot
from telegram.ext.utils.types import UD, CD, BD
from telegram.ext.utils.types import UD, CD, BD, BT, JQ, PT # pylint: disable=unused-import
if TYPE_CHECKING:
from telegram import Bot
from telegram.ext import Dispatcher, Job, JobQueue
from telegram.ext.utils.types import CCT
class CallbackContext(Generic[UD, CD, BD]):
class CallbackContext(Generic[BT, UD, CD, BD]):
"""
This is a context object passed to the callback called by :class:`telegram.ext.Handler`
or by the :class:`telegram.ext.Dispatcher` in an error handler added by
@ -94,6 +93,26 @@ class CallbackContext(Generic[UD, CD, BD]):
"""
if TYPE_CHECKING:
DEFAULT_TYPE = CallbackContext[ # type: ignore[misc] # noqa: F821
ExtBot, Dict, Dict, Dict
]
else:
# Somewhat silly workaround so that accessing the attribute
# doesn't only work while type checking
DEFAULT_TYPE = 'CallbackContext[ExtBot, Dict, Dict, Dict]' # pylint: disable-all
"""Shortcut for the type annotation for the `context` argument that's correct for the
default settings, i.e. if :class:`telegram.ext.ContextTypes` is not used.
Example:
.. code:: python
def callback(update: Update, context: CallbackContext.DEFAULT_TYPE):
...
.. versionadded: 14.0
"""
__slots__ = (
'_dispatcher',
'_chat_id_and_data',
@ -107,7 +126,7 @@ class CallbackContext(Generic[UD, CD, BD]):
'__dict__',
)
def __init__(self: 'CCT', dispatcher: 'Dispatcher[CCT, UD, CD, BD]'):
def __init__(self: 'CCT', dispatcher: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]'):
"""
Args:
dispatcher (:class:`telegram.ext.Dispatcher`):
@ -123,7 +142,7 @@ class CallbackContext(Generic[UD, CD, BD]):
self.async_kwargs: Optional[Dict[str, object]] = None
@property
def dispatcher(self) -> 'Dispatcher[CCT, UD, CD, BD]':
def dispatcher(self) -> 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]':
""":class:`telegram.ext.Dispatcher`: The dispatcher associated with this context."""
return self._dispatcher
@ -232,7 +251,7 @@ class CallbackContext(Generic[UD, CD, BD]):
cls: Type['CCT'],
update: object,
error: Exception,
dispatcher: 'Dispatcher[CCT, UD, CD, BD]',
dispatcher: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]',
async_args: Union[List, Tuple] = None,
async_kwargs: Dict[str, object] = None,
job: 'Job' = None,
@ -271,7 +290,7 @@ class CallbackContext(Generic[UD, CD, BD]):
@classmethod
def from_update(
cls: Type['CCT'], update: object, dispatcher: 'Dispatcher[CCT, UD, CD, BD]'
cls: Type['CCT'], update: object, dispatcher: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]'
) -> 'CCT':
"""
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the
@ -306,7 +325,9 @@ class CallbackContext(Generic[UD, CD, BD]):
return self
@classmethod
def from_job(cls: Type['CCT'], job: 'Job', dispatcher: 'Dispatcher[CCT, UD, CD, BD]') -> 'CCT':
def from_job(
cls: Type['CCT'], job: 'Job', dispatcher: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]'
) -> 'CCT':
"""
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to a
job callback.
@ -335,7 +356,7 @@ class CallbackContext(Generic[UD, CD, BD]):
setattr(self, key, value)
@property
def bot(self) -> 'Bot':
def bot(self) -> BT:
""":class:`telegram.Bot`: The bot associated with this context."""
return self._dispatcher.bot

View file

@ -21,6 +21,7 @@
from typing import Type, Generic, overload, Dict # pylint: disable=unused-import
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.extbot import ExtBot # pylint: disable=unused-import
from telegram.ext.utils.types import CCT, UD, CD, BD
@ -54,7 +55,7 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
@overload
def __init__(
self: 'ContextTypes[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]',
self: 'ContextTypes[CallbackContext[ExtBot, Dict, Dict, Dict], Dict, Dict, Dict]',
):
...
@ -64,19 +65,22 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
@overload
def __init__(
self: 'ContextTypes[CallbackContext[UD, Dict, Dict], UD, Dict, Dict]', user_data: Type[UD]
self: 'ContextTypes[CallbackContext[ExtBot, UD, Dict, Dict], UD, Dict, Dict]',
user_data: Type[UD],
):
...
@overload
def __init__(
self: 'ContextTypes[CallbackContext[Dict, CD, Dict], Dict, CD, Dict]', chat_data: Type[CD]
self: 'ContextTypes[CallbackContext[ExtBot, Dict, CD, Dict], Dict, CD, Dict]',
chat_data: Type[CD],
):
...
@overload
def __init__(
self: 'ContextTypes[CallbackContext[Dict, Dict, BD], Dict, Dict, BD]', bot_data: Type[BD]
self: 'ContextTypes[CallbackContext[ExtBot, Dict, Dict, BD], Dict, Dict, BD]',
bot_data: Type[BD],
):
...
@ -100,7 +104,7 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
@overload
def __init__(
self: 'ContextTypes[CallbackContext[UD, CD, Dict], UD, CD, Dict]',
self: 'ContextTypes[CallbackContext[ExtBot, UD, CD, Dict], UD, CD, Dict]',
user_data: Type[UD],
chat_data: Type[CD],
):
@ -108,7 +112,7 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
@overload
def __init__(
self: 'ContextTypes[CallbackContext[UD, Dict, BD], UD, Dict, BD]',
self: 'ContextTypes[CallbackContext[ExtBot, UD, Dict, BD], UD, Dict, BD]',
user_data: Type[UD],
bot_data: Type[BD],
):
@ -116,7 +120,7 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
@overload
def __init__(
self: 'ContextTypes[CallbackContext[Dict, CD, BD], Dict, CD, BD]',
self: 'ContextTypes[CallbackContext[ExtBot, Dict, CD, BD], Dict, CD, BD]',
chat_data: Type[CD],
bot_data: Type[BD],
):
@ -151,7 +155,7 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
@overload
def __init__(
self: 'ContextTypes[CallbackContext[UD, CD, BD], UD, CD, BD]',
self: 'ContextTypes[CallbackContext[ExtBot, UD, CD, BD], UD, CD, BD]',
user_data: Type[UD],
chat_data: Type[CD],
bot_data: Type[BD],

View file

@ -23,7 +23,18 @@ import logging
import functools
import datetime
from threading import Lock
from typing import TYPE_CHECKING, Dict, List, NoReturn, Optional, Union, Tuple, cast, ClassVar
from typing import ( # pylint: disable=unused-import # for the "Any" import
TYPE_CHECKING,
Dict,
List,
NoReturn,
Optional,
Union,
Tuple,
cast,
ClassVar,
Any,
)
from telegram import Update
from telegram.ext import (
@ -41,7 +52,7 @@ from telegram.ext.utils.types import CCT
from telegram.utils.warnings import warn
if TYPE_CHECKING:
from telegram.ext import Dispatcher, Job
from telegram.ext import Dispatcher, Job, JobQueue
CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]]
@ -52,7 +63,7 @@ class _ConversationTimeoutContext:
self,
conversation_key: Tuple[int, ...],
update: Update,
dispatcher: 'Dispatcher',
dispatcher: 'Dispatcher[Any, CCT, Any, Any, Any, JobQueue, Any]',
callback_context: CallbackContext,
):
self.conversation_key = conversation_key
@ -489,7 +500,7 @@ class ConversationHandler(Handler[Update, CCT]):
def _schedule_job(
self,
new_state: object,
dispatcher: 'Dispatcher',
dispatcher: 'Dispatcher[Any, CCT, Any, Any, Any, JobQueue, Any]',
update: Update,
context: CallbackContext,
conversation_key: Tuple[int, ...],
@ -498,7 +509,7 @@ class ConversationHandler(Handler[Update, CCT]):
try:
# both job_queue & conversation_timeout are checked before calling _schedule_job
j_queue = dispatcher.job_queue
self.timeout_jobs[conversation_key] = j_queue.run_once( # type: ignore[union-attr]
self.timeout_jobs[conversation_key] = j_queue.run_once(
self._trigger_timeout,
self.conversation_timeout, # type: ignore[arg-type]
context=_ConversationTimeoutContext(

View file

@ -17,15 +17,15 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the Dispatcher class."""
import inspect
import logging
import weakref
from collections import defaultdict
from pathlib import Path
from queue import Empty, Queue
from threading import BoundedSemaphore, Event, Lock, Thread, current_thread
from time import sleep
from typing import (
TYPE_CHECKING,
Callable,
DefaultDict,
Dict,
@ -35,8 +35,7 @@ from typing import (
Union,
Generic,
TypeVar,
overload,
cast,
TYPE_CHECKING,
)
from uuid import uuid4
@ -48,11 +47,12 @@ from telegram.ext.callbackdatacache import CallbackDataCache
from telegram.utils.defaultvalue import DefaultValue, DEFAULT_FALSE
from telegram.utils.warnings import warn
from telegram.ext.utils.promise import Promise
from telegram.ext.utils.types import CCT, UD, CD, BD
from telegram.ext.utils.types import CCT, UD, CD, BD, BT, JQ, PT
from .utils.stack import was_called_by
if TYPE_CHECKING:
from telegram import Bot
from telegram.ext import JobQueue, Job, CallbackContext
from .jobqueue import Job
from .builders import InitDispatcherBuilder
DEFAULT_GROUP: int = 0
@ -90,24 +90,15 @@ class DispatcherHandlerStop(Exception):
self.state = state
class Dispatcher(Generic[CCT, UD, CD, BD]):
class Dispatcher(Generic[BT, CCT, UD, CD, BD, JQ, PT]):
"""This class dispatches all kinds of updates to its registered handlers.
Args:
bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers.
update_queue (:obj:`Queue`): The synchronized queue that will contain the updates.
job_queue (:class:`telegram.ext.JobQueue`, optional): The :class:`telegram.ext.JobQueue`
instance to pass onto handler callbacks.
workers (:obj:`int`, optional): Number of maximum concurrent worker threads for the
``@run_async`` decorator and :meth:`run_async`. Defaults to 4.
persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to
store data that should be persistent over restarts.
context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance
of :class:`telegram.ext.ContextTypes` to customize the types used in the
``context`` interface. If not passed, the defaults documented in
:class:`telegram.ext.ContextTypes` will be used.
Note:
This class may not be initialized directly. Use :class:`telegram.ext.DispatcherBuilder` or
:meth:`builder` (for convenience).
.. versionadded:: 13.6
.. versionchanged:: 14.0
Initialization is now done through the :class:`telegram.ext.DispatcherBuilder`.
Attributes:
bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers.
@ -121,10 +112,29 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot.
persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
store data that should be persistent over restarts.
context_types (:class:`telegram.ext.ContextTypes`): Container for the types used
in the ``context`` interface.
exception_event (:class:`threading.Event`): When this event is set, the dispatcher will
stop processing updates. If this dispatcher is used together with an
:class:`telegram.ext.Updater`, then this event will be the same object as
:attr:`telegram.ext.Updater.exception_event`.
handlers (Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]): A dictionary mapping each
handler group to the list of handlers registered to that group.
.. versionadded:: 13.6
.. seealso::
:meth:`add_handler`
groups (List[:obj:`int`]): A list of all handler groups that have handlers registered.
.. seealso::
:meth:`add_handler`
error_handlers (Dict[:obj:`callable`, :obj:`bool`]): A dict, where the keys are error
handlers and the values indicate whether they are to be run asynchronously via
:meth:`run_async`.
.. seealso::
:meth:`add_error_handler`
running (:obj:`bool`): Indicates if this dispatcher is running.
.. seealso::
:meth:`start`, :meth:`stop`
"""
@ -143,7 +153,7 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
'error_handlers',
'running',
'__stop_event',
'__exception_event',
'exception_event',
'__async_queue',
'__async_threads',
'bot',
@ -156,51 +166,37 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
__singleton = None
logger = logging.getLogger(__name__)
@overload
def __init__(
self: 'Dispatcher[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]',
bot: 'Bot',
self: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]',
*,
bot: BT,
update_queue: Queue,
workers: int = 4,
exception_event: Event = None,
job_queue: 'JobQueue' = None,
persistence: BasePersistence = None,
job_queue: JQ,
workers: int,
persistence: PT,
context_types: ContextTypes[CCT, UD, CD, BD],
exception_event: Event,
stack_level: int = 4,
):
...
if not was_called_by(
inspect.currentframe(), Path(__file__).parent.resolve() / 'builders.py'
):
warn(
'`Dispatcher` instances should be built via the `DispatcherBuilder`.',
stacklevel=2,
)
@overload
def __init__(
self: 'Dispatcher[CCT, UD, CD, BD]',
bot: 'Bot',
update_queue: Queue,
workers: int = 4,
exception_event: Event = None,
job_queue: 'JobQueue' = None,
persistence: BasePersistence = None,
context_types: ContextTypes[CCT, UD, CD, BD] = None,
):
...
def __init__(
self,
bot: 'Bot',
update_queue: Queue,
workers: int = 4,
exception_event: Event = None,
job_queue: 'JobQueue' = None,
persistence: BasePersistence = None,
context_types: ContextTypes[CCT, UD, CD, BD] = None,
):
self.bot = bot
self.update_queue = update_queue
self.job_queue = job_queue
self.workers = workers
self.context_types = cast(ContextTypes[CCT, UD, CD, BD], context_types or ContextTypes())
self.context_types = context_types
self.exception_event = exception_event
if self.workers < 1:
warn(
'Asynchronous callbacks can not be processed without at least one worker thread.',
stacklevel=2,
stacklevel=stack_level,
)
self.user_data: DefaultDict[int, UD] = defaultdict(self.context_types.user_data)
@ -211,8 +207,12 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
if persistence:
if not isinstance(persistence, BasePersistence):
raise TypeError("persistence must be based on telegram.ext.BasePersistence")
self.persistence = persistence
# This raises an exception if persistence.store_data.callback_data is True
# but self.bot is not an instance of ExtBot - so no need to check that later on
self.persistence.set_bot(self.bot)
if self.persistence.store_data.user_data:
self.user_data = self.persistence.get_user_data()
if not isinstance(self.user_data, defaultdict):
@ -228,31 +228,26 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
f"bot_data must be of type {self.context_types.bot_data.__name__}"
)
if self.persistence.store_data.callback_data:
self.bot = cast(ExtBot, self.bot)
persistent_data = self.persistence.get_callback_data()
if persistent_data is not None:
if not isinstance(persistent_data, tuple) and len(persistent_data) != 2:
raise ValueError('callback_data must be a 2-tuple')
self.bot.callback_data_cache = CallbackDataCache(
self.bot,
self.bot.callback_data_cache.maxsize,
# Mypy doesn't know that persistence.set_bot (see above) already checks that
# self.bot is an instance of ExtBot if callback_data should be stored ...
self.bot.callback_data_cache = CallbackDataCache( # type: ignore[attr-defined]
self.bot, # type: ignore[arg-type]
self.bot.callback_data_cache.maxsize, # type: ignore[attr-defined]
persistent_data=persistent_data,
)
else:
self.persistence = None
self.handlers: Dict[int, List[Handler]] = {}
"""Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]: Holds the handlers per group."""
self.groups: List[int] = []
"""List[:obj:`int`]: A list with all groups."""
self.error_handlers: Dict[Callable, Union[bool, DefaultValue]] = {}
"""Dict[:obj:`callable`, :obj:`bool`]: A dict, where the keys are error handlers and the
values indicate whether they are to be run asynchronously."""
self.running = False
""":obj:`bool`: Indicates if this dispatcher is running."""
self.__stop_event = Event()
self.__exception_event = exception_event or Event()
self.__async_queue: Queue = Queue()
self.__async_threads: Set[Thread] = set()
@ -265,9 +260,16 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
else:
self._set_singleton(None)
@property
def exception_event(self) -> Event: # skipcq: PY-D0003
return self.__exception_event
@staticmethod
def builder() -> 'InitDispatcherBuilder':
"""Convenience method. Returns a new :class:`telegram.ext.DispatcherBuilder`.
.. versionadded:: 14.0
"""
# Unfortunately this needs to be here due to cyclical imports
from telegram.ext import DispatcherBuilder # pylint: disable=import-outside-toplevel
return DispatcherBuilder()
def _init_async_threads(self, base_name: str, workers: int) -> None:
base_name = f'{base_name}_' if base_name else ''
@ -368,7 +370,7 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
def start(self, ready: Event = None) -> None:
"""Thread target of thread 'dispatcher'.
Runs in background and processes the update queue.
Runs in background and processes the update queue. Also starts :attr:`job_queue`, if set.
Args:
ready (:obj:`threading.Event`, optional): If specified, the event will be set once the
@ -381,11 +383,13 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
ready.set()
return
if self.__exception_event.is_set():
if self.exception_event.is_set():
msg = 'reusing dispatcher after exception event is forbidden'
self.logger.error(msg)
raise TelegramError(msg)
if self.job_queue:
self.job_queue.start()
self._init_async_threads(str(uuid4()), self.workers)
self.running = True
self.logger.debug('Dispatcher started')
@ -401,7 +405,7 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
if self.__stop_event.is_set():
self.logger.debug('orderly stopping')
break
if self.__exception_event.is_set():
if self.exception_event.is_set():
self.logger.critical('stopping due to exception in another thread')
break
continue
@ -414,7 +418,10 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
self.logger.debug('Dispatcher thread stopped')
def stop(self) -> None:
"""Stops the thread."""
"""Stops the thread and :attr:`job_queue`, if set.
Also calls :meth:`update_persistence` and :meth:`BasePersistence.flush` on
:attr:`persistence`, if set.
"""
if self.running:
self.__stop_event.set()
while self.running:
@ -436,6 +443,17 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
self.__async_threads.remove(thr)
self.logger.debug('async thread %s/%s has ended', i + 1, total)
if self.job_queue:
self.job_queue.stop()
self.logger.debug('JobQueue was shut down.')
self.update_persistence()
if self.persistence:
self.persistence.flush()
# Clear the connection pool
self.bot.request.stop()
@property
def has_running_threads(self) -> bool: # skipcq: PY-D0003
return self.running or bool(self.__async_threads)
@ -602,10 +620,11 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
user_ids = []
if self.persistence.store_data.callback_data:
self.bot = cast(ExtBot, self.bot)
try:
# Mypy doesn't know that persistence.set_bot (see above) already checks that
# self.bot is an instance of ExtBot if callback_data should be stored ...
self.persistence.update_callback_data(
self.bot.callback_data_cache.persistence_data
self.bot.callback_data_cache.persistence_data # type: ignore[attr-defined]
)
except Exception as exc:
self.dispatch_error(update, exc)
@ -641,11 +660,8 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
Args:
callback (:obj:`callable`): The callback function for this error handler. Will be
called when an error is raised.
Callback signature:
``def callback(update: Update, context: CallbackContext)``
called when an error is raised. Callback signature:
``def callback(update: Update, context: CallbackContext)``
The error that happened will be present in context.error.
run_async (:obj:`bool`, optional): Whether this handlers callback should be run

View file

@ -94,8 +94,8 @@ class ExtBot(telegram.bot.Bot):
def __init__(
self,
token: str,
base_url: str = None,
base_file_url: str = None,
base_url: str = 'https://api.telegram.org/bot',
base_file_url: str = 'https://api.telegram.org/file/bot',
request: 'Request' = None,
private_key: bytes = None,
private_key_password: bytes = None,

View file

@ -1166,23 +1166,23 @@ officedocument.wordprocessingml.document")``.
name = 'Filters.status_update'
def filter(self, message: Update) -> bool:
def filter(self, update: Update) -> bool:
return bool(
self.new_chat_members(message)
or self.left_chat_member(message)
or self.new_chat_title(message)
or self.new_chat_photo(message)
or self.delete_chat_photo(message)
or self.chat_created(message)
or self.message_auto_delete_timer_changed(message)
or self.migrate(message)
or self.pinned_message(message)
or self.connected_website(message)
or self.proximity_alert_triggered(message)
or self.voice_chat_scheduled(message)
or self.voice_chat_started(message)
or self.voice_chat_ended(message)
or self.voice_chat_participants_invited(message)
self.new_chat_members(update)
or self.left_chat_member(update)
or self.new_chat_title(update)
or self.new_chat_photo(update)
or self.delete_chat_photo(update)
or self.chat_created(update)
or self.message_auto_delete_timer_changed(update)
or self.migrate(update)
or self.pinned_message(update)
or self.connected_website(update)
or self.proximity_alert_triggered(update)
or self.voice_chat_scheduled(update)
or self.voice_chat_started(update)
or self.voice_chat_ended(update)
or self.voice_chat_participants_invited(update)
)
status_update = _StatusUpdate()

View file

@ -19,6 +19,7 @@
"""This module contains the classes JobQueue and Job."""
import datetime
import weakref
from typing import TYPE_CHECKING, Callable, Optional, Tuple, Union, cast, overload
import pytz
@ -45,7 +46,7 @@ class JobQueue:
__slots__ = ('_dispatcher', 'scheduler')
def __init__(self) -> None:
self._dispatcher: 'Dispatcher' = None # type: ignore[assignment]
self._dispatcher: 'Optional[weakref.ReferenceType[Dispatcher]]' = None
self.scheduler = BackgroundScheduler(timezone=pytz.utc)
def _tz_now(self) -> datetime.datetime:
@ -93,10 +94,20 @@ class JobQueue:
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher.
"""
self._dispatcher = dispatcher
self._dispatcher = weakref.ref(dispatcher)
if isinstance(dispatcher.bot, ExtBot) and dispatcher.bot.defaults:
self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc)
@property
def dispatcher(self) -> 'Dispatcher':
"""The dispatcher this JobQueue is associated with."""
if self._dispatcher is None:
raise RuntimeError('No dispatcher was set for this JobQueue.')
dispatcher = self._dispatcher()
if dispatcher is not None:
return dispatcher
raise RuntimeError('The dispatcher instance is no longer alive.')
def run_once(
self,
callback: Callable[['CallbackContext'], None],
@ -151,7 +162,7 @@ class JobQueue:
name=name,
trigger='date',
run_date=date_time,
args=(self._dispatcher,),
args=(self.dispatcher,),
timezone=date_time.tzinfo or self.scheduler.timezone,
**job_kwargs,
)
@ -241,7 +252,7 @@ class JobQueue:
j = self.scheduler.add_job(
job,
trigger='interval',
args=(self._dispatcher,),
args=(self.dispatcher,),
start_date=dt_first,
end_date=dt_last,
seconds=interval,
@ -297,7 +308,7 @@ class JobQueue:
j = self.scheduler.add_job(
job,
trigger='cron',
args=(self._dispatcher,),
args=(self.dispatcher,),
name=name,
day='last' if day == -1 else day,
hour=when.hour,
@ -354,7 +365,7 @@ class JobQueue:
j = self.scheduler.add_job(
job,
name=name,
args=(self._dispatcher,),
args=(self.dispatcher,),
trigger='cron',
day_of_week=','.join([str(d) for d in days]),
hour=time.hour,
@ -394,7 +405,7 @@ class JobQueue:
name = name or callback.__name__
job = Job(callback, context, name)
j = self.scheduler.add_job(job, args=(self._dispatcher,), name=name, **job_kwargs)
j = self.scheduler.add_job(job, args=(self.dispatcher,), name=name, **job_kwargs)
job.job = j
return job

View file

@ -17,42 +17,42 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the class Updater, which tries to make creating Telegram bots intuitive."""
import inspect
import logging
import ssl
import signal
from pathlib import Path
from queue import Queue
from threading import Event, Lock, Thread, current_thread
from time import sleep
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Optional,
Tuple,
Union,
no_type_check,
Generic,
overload,
TypeVar,
TYPE_CHECKING,
)
from telegram import Bot
from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized, TelegramError
from telegram.ext import Dispatcher, JobQueue, ContextTypes, ExtBot
from telegram.warnings import PTBDeprecationWarning
from telegram.request import Request
from telegram.utils.defaultvalue import DEFAULT_FALSE, DefaultValue
from telegram.utils.warnings import warn
from telegram.ext.utils.types import CCT, UD, CD, BD
from telegram.ext import Dispatcher
from telegram.ext.utils.webhookhandler import WebhookAppClass, WebhookServer
from .utils.stack import was_called_by
from .utils.types import BT
from ..utils.warnings import warn
if TYPE_CHECKING:
from telegram.ext import BasePersistence, Defaults, CallbackContext
from .builders import InitUpdaterBuilder
class Updater(Generic[CCT, UD, CD, BD]):
DT = TypeVar('DT', bound=Union[None, Dispatcher])
class Updater(Generic[BT, DT]):
"""
This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to
:class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to
@ -64,260 +64,95 @@ class Updater(Generic[CCT, UD, CD, BD]):
WebhookHandler classes.
Note:
* You must supply either a :attr:`bot` or a :attr:`token` argument.
* If you supply a :attr:`bot`, you will need to pass :attr:`arbitrary_callback_data`,
and :attr:`defaults` to the bot instead of the :class:`telegram.ext.Updater`. In this
case, you'll have to use the class :class:`telegram.ext.ExtBot`.
.. versionchanged:: 13.6
Args:
token (:obj:`str`, optional): The bot's token given by the @BotFather.
base_url (:obj:`str`, optional): Base_url for the bot.
base_file_url (:obj:`str`, optional): Base_file_url for the bot.
workers (:obj:`int`, optional): Amount of threads in the thread pool for functions
decorated with ``@run_async`` (ignored if `dispatcher` argument is used).
bot (:class:`telegram.Bot`, optional): A pre-initialized bot instance (ignored if
`dispatcher` argument is used). If a pre-initialized bot is used, it is the user's
responsibility to create it using a `Request` instance with a large enough connection
pool.
dispatcher (:class:`telegram.ext.Dispatcher`, optional): A pre-initialized dispatcher
instance. If a pre-initialized dispatcher is used, it is the user's responsibility to
create it with proper arguments.
private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data.
private_key_password (:obj:`bytes`, optional): Password for above private key.
user_sig_handler (:obj:`function`, optional): Takes ``signum, frame`` as positional
arguments. This will be called when a signal is received, defaults are (SIGINT,
SIGTERM, SIGABRT) settable with :attr:`idle`.
request_kwargs (:obj:`dict`, optional): Keyword args to control the creation of a
`telegram.request.Request` object (ignored if `bot` or `dispatcher` argument is
used). The request_kwargs are very useful for the advanced users who would like to
control the default timeouts and/or control the proxy used for http communication.
persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to
store data that should be persistent over restarts (ignored if `dispatcher` argument is
used).
defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to
be used if not set explicitly in the bot methods.
arbitrary_callback_data (:obj:`bool` | :obj:`int` | :obj:`None`, optional): Whether to
allow arbitrary objects as callback data for :class:`telegram.InlineKeyboardButton`.
Pass an integer to specify the maximum number of cached objects. For more details,
please see our wiki. Defaults to :obj:`False`.
.. versionadded:: 13.6
context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance
of :class:`telegram.ext.ContextTypes` to customize the types used in the
``context`` interface. If not passed, the defaults documented in
:class:`telegram.ext.ContextTypes` will be used.
.. versionadded:: 13.6
Raises:
ValueError: If both :attr:`token` and :attr:`bot` are passed or none of them.
This class may not be initialized directly. Use :class:`telegram.ext.UpdaterBuilder` or
:meth:`builder` (for convenience).
.. versionchanged:: 14.0
* Initialization is now done through the :class:`telegram.ext.UpdaterBuilder`.
* Renamed ``user_sig_handler`` to :attr:`user_signal_handler`.
* Removed the attributes ``job_queue``, and ``persistence`` - use the corresponding
attributes of :attr:`dispatcher` instead.
Attributes:
bot (:class:`telegram.Bot`): The bot used with this Updater.
user_sig_handler (:obj:`function`): Optional. Function to be called when a signal is
user_signal_handler (:obj:`function`): Optional. Function to be called when a signal is
received.
.. versionchanged:: 14.0
Renamed ``user_sig_handler`` to ``user_signal_handler``.
update_queue (:obj:`Queue`): Queue for the updates.
job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and
dispatches them to the handlers.
dispatcher (:class:`telegram.ext.Dispatcher`): Optional. Dispatcher that handles the
updates and dispatches them to the handlers.
running (:obj:`bool`): Indicates if the updater is running.
persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
store data that should be persistent over restarts.
exception_event (:class:`threading.Event`): When an unhandled exception happens while
fetching updates, this event will be set. If :attr:`dispatcher` is not :obj:`None`, it
is the same object as :attr:`telegram.ext.Dispatcher.exception_event`.
.. versionadded:: 14.0
"""
__slots__ = (
'persistence',
'dispatcher',
'user_sig_handler',
'user_signal_handler',
'bot',
'logger',
'update_queue',
'job_queue',
'__exception_event',
'exception_event',
'last_update_id',
'running',
'_request',
'is_idle',
'httpd',
'__lock',
'__threads',
)
@overload
def __init__(
self: 'Updater[CallbackContext, dict, dict, dict]',
token: str = None,
base_url: str = None,
workers: int = 4,
bot: Bot = None,
private_key: bytes = None,
private_key_password: bytes = None,
user_sig_handler: Callable = None,
request_kwargs: Dict[str, Any] = None,
persistence: 'BasePersistence' = None, # pylint: disable=used-before-assignment
defaults: 'Defaults' = None,
base_file_url: str = None,
arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE,
self: 'Updater[BT, DT]',
*,
user_signal_handler: Callable[[int, object], Any] = None,
dispatcher: DT = None,
bot: BT = None,
update_queue: Queue = None,
exception_event: Event = None,
):
...
@overload
def __init__(
self: 'Updater[CCT, UD, CD, BD]',
token: str = None,
base_url: str = None,
workers: int = 4,
bot: Bot = None,
private_key: bytes = None,
private_key_password: bytes = None,
user_sig_handler: Callable = None,
request_kwargs: Dict[str, Any] = None,
persistence: 'BasePersistence' = None,
defaults: 'Defaults' = None,
base_file_url: str = None,
arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE,
context_types: ContextTypes[CCT, UD, CD, BD] = None,
):
...
@overload
def __init__(
self: 'Updater[CCT, UD, CD, BD]',
user_sig_handler: Callable = None,
dispatcher: Dispatcher[CCT, UD, CD, BD] = None,
):
...
def __init__( # type: ignore[no-untyped-def,misc]
self,
token: str = None,
base_url: str = None,
workers: int = 4,
bot: Bot = None,
private_key: bytes = None,
private_key_password: bytes = None,
user_sig_handler: Callable = None,
request_kwargs: Dict[str, Any] = None,
persistence: 'BasePersistence' = None,
defaults: 'Defaults' = None,
dispatcher=None,
base_file_url: str = None,
arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE,
context_types: ContextTypes[CCT, UD, CD, BD] = None,
):
if defaults and bot:
if not was_called_by(
inspect.currentframe(), Path(__file__).parent.resolve() / 'builders.py'
):
warn(
'Passing defaults to an Updater has no effect when a Bot is passed '
'as well. Pass them to the Bot instead.',
PTBDeprecationWarning,
stacklevel=2,
)
if arbitrary_callback_data is not DEFAULT_FALSE and bot:
warn(
'Passing arbitrary_callback_data to an Updater has no '
'effect when a Bot is passed as well. Pass them to the Bot instead.',
'`Updater` instances should be built via the `UpdaterBuilder`.',
stacklevel=2,
)
if dispatcher is None:
if (token is None) and (bot is None):
raise ValueError('`token` or `bot` must be passed')
if (token is not None) and (bot is not None):
raise ValueError('`token` and `bot` are mutually exclusive')
if (private_key is not None) and (bot is not None):
raise ValueError('`bot` and `private_key` are mutually exclusive')
self.user_signal_handler = user_signal_handler
self.dispatcher = dispatcher
if self.dispatcher:
self.bot = self.dispatcher.bot
self.update_queue = self.dispatcher.update_queue
self.exception_event = self.dispatcher.exception_event
else:
if bot is not None:
raise ValueError('`dispatcher` and `bot` are mutually exclusive')
if persistence is not None:
raise ValueError('`dispatcher` and `persistence` are mutually exclusive')
if context_types is not None:
raise ValueError('`dispatcher` and `context_types` are mutually exclusive')
if workers is not None:
raise ValueError('`dispatcher` and `workers` are mutually exclusive')
self.bot = bot
self.update_queue = update_queue
self.exception_event = exception_event
self.logger = logging.getLogger(__name__)
self._request = None
if dispatcher is None:
con_pool_size = workers + 4
if bot is not None:
self.bot = bot
if bot.request.con_pool_size < con_pool_size:
warn(
f'Connection pool of Request object is smaller than optimal value '
f'{con_pool_size}',
stacklevel=2,
)
else:
# we need a connection pool the size of:
# * for each of the workers
# * 1 for Dispatcher
# * 1 for polling Updater (even if webhook is used, we can spare a connection)
# * 1 for JobQueue
# * 1 for main thread
if request_kwargs is None:
request_kwargs = {}
if 'con_pool_size' not in request_kwargs:
request_kwargs['con_pool_size'] = con_pool_size
self._request = Request(**request_kwargs)
self.bot = ExtBot(
token, # type: ignore[arg-type]
base_url,
base_file_url=base_file_url,
request=self._request,
private_key=private_key,
private_key_password=private_key_password,
defaults=defaults,
arbitrary_callback_data=(
False # type: ignore[arg-type]
if arbitrary_callback_data is DEFAULT_FALSE
else arbitrary_callback_data
),
)
self.update_queue: Queue = Queue()
self.job_queue = JobQueue()
self.__exception_event = Event()
self.persistence = persistence
self.dispatcher = Dispatcher(
self.bot,
self.update_queue,
job_queue=self.job_queue,
workers=workers,
exception_event=self.__exception_event,
persistence=persistence,
context_types=context_types,
)
self.job_queue.set_dispatcher(self.dispatcher)
else:
con_pool_size = dispatcher.workers + 4
self.bot = dispatcher.bot
if self.bot.request.con_pool_size < con_pool_size:
warn(
f'Connection pool of Request object is smaller than optimal value '
f'{con_pool_size}',
stacklevel=2,
)
self.update_queue = dispatcher.update_queue
self.__exception_event = dispatcher.exception_event
self.persistence = dispatcher.persistence
self.job_queue = dispatcher.job_queue
self.dispatcher = dispatcher
self.user_sig_handler = user_sig_handler
self.last_update_id = 0
self.running = False
self.is_idle = False
self.httpd = None
self.__lock = Lock()
self.__threads: List[Thread] = []
self.logger = logging.getLogger(__name__)
@staticmethod
def builder() -> 'InitUpdaterBuilder':
"""Convenience method. Returns a new :class:`telegram.ext.UpdaterBuilder`.
.. versionadded:: 14.0
"""
# Unfortunately this needs to be here due to cyclical imports
from telegram.ext import UpdaterBuilder # pylint: disable=import-outside-toplevel
return UpdaterBuilder()
def _init_thread(self, target: Callable, name: str, *args: object, **kwargs: object) -> None:
thr = Thread(
@ -335,7 +170,7 @@ class Updater(Generic[CCT, UD, CD, BD]):
try:
target(*args, **kwargs)
except Exception:
self.__exception_event.set()
self.exception_event.set()
self.logger.exception('unhandled exception in %s', thr_name)
raise
self.logger.debug('%s - ended', thr_name)
@ -384,10 +219,11 @@ class Updater(Generic[CCT, UD, CD, BD]):
self.running = True
# Create & start threads
self.job_queue.start()
dispatcher_ready = Event()
polling_ready = Event()
self._init_thread(self.dispatcher.start, "dispatcher", ready=dispatcher_ready)
if self.dispatcher:
self._init_thread(self.dispatcher.start, "dispatcher", ready=dispatcher_ready)
self._init_thread(
self._start_polling,
"updater",
@ -400,9 +236,11 @@ class Updater(Generic[CCT, UD, CD, BD]):
ready=polling_ready,
)
self.logger.debug('Waiting for Dispatcher and polling to start')
dispatcher_ready.wait()
self.logger.debug('Waiting for polling to start')
polling_ready.wait()
if self.dispatcher:
self.logger.debug('Waiting for Dispatcher to start')
dispatcher_ready.wait()
# Return the update queue so the main thread can insert updates
return self.update_queue
@ -478,8 +316,9 @@ class Updater(Generic[CCT, UD, CD, BD]):
# Create & start threads
webhook_ready = Event()
dispatcher_ready = Event()
self.job_queue.start()
self._init_thread(self.dispatcher.start, "dispatcher", dispatcher_ready)
if self.dispatcher:
self._init_thread(self.dispatcher.start, "dispatcher", dispatcher_ready)
self._init_thread(
self._start_webhook,
"updater",
@ -497,9 +336,11 @@ class Updater(Generic[CCT, UD, CD, BD]):
max_connections=max_connections,
)
self.logger.debug('Waiting for Dispatcher and Webhook to start')
self.logger.debug('Waiting for webhook to start')
webhook_ready.wait()
dispatcher_ready.wait()
if self.dispatcher:
self.logger.debug('Waiting for Dispatcher to start')
dispatcher_ready.wait()
# Return the update queue so the main thread can insert updates
return self.update_queue
@ -661,18 +502,26 @@ class Updater(Generic[CCT, UD, CD, BD]):
webhook_url = self._gen_webhook_url(listen, port, url_path)
# We pass along the cert to the webhook if present.
cert_file = open(cert, 'rb') if cert is not None else None
self._bootstrap(
max_retries=bootstrap_retries,
drop_pending_updates=drop_pending_updates,
webhook_url=webhook_url,
allowed_updates=allowed_updates,
cert=cert_file,
ip_address=ip_address,
max_connections=max_connections,
)
if cert_file is not None:
cert_file.close()
if cert is not None:
with open(cert, 'rb') as cert_file:
self._bootstrap(
cert=cert_file,
max_retries=bootstrap_retries,
drop_pending_updates=drop_pending_updates,
webhook_url=webhook_url,
allowed_updates=allowed_updates,
ip_address=ip_address,
max_connections=max_connections,
)
else:
self._bootstrap(
max_retries=bootstrap_retries,
drop_pending_updates=drop_pending_updates,
webhook_url=webhook_url,
allowed_updates=allowed_updates,
ip_address=ip_address,
max_connections=max_connections,
)
self.httpd.serve_forever(ready=ready)
@ -750,10 +599,11 @@ class Updater(Generic[CCT, UD, CD, BD]):
def stop(self) -> None:
"""Stops the polling/webhook thread, the dispatcher and the job queue."""
self.job_queue.stop()
with self.__lock:
if self.running or self.dispatcher.has_running_threads:
self.logger.debug('Stopping Updater and Dispatcher...')
if self.running or (self.dispatcher and self.dispatcher.has_running_threads):
self.logger.debug(
'Stopping Updater %s...', 'and Dispatcher ' if self.dispatcher else ''
)
self.running = False
@ -761,9 +611,10 @@ class Updater(Generic[CCT, UD, CD, BD]):
self._stop_dispatcher()
self._join_threads()
# Stop the Request instance only if it was created by the Updater
if self._request:
self._request.stop()
# Clear the connection pool only if the bot is managed by the Updater
# Otherwise `dispatcher.stop()` already does that
if not self.dispatcher:
self.bot.request.stop()
@no_type_check
def _stop_httpd(self) -> None:
@ -778,8 +629,9 @@ class Updater(Generic[CCT, UD, CD, BD]):
@no_type_check
def _stop_dispatcher(self) -> None:
self.logger.debug('Requesting Dispatcher to stop...')
self.dispatcher.stop()
if self.dispatcher:
self.logger.debug('Requesting Dispatcher to stop...')
self.dispatcher.stop()
@no_type_check
def _join_threads(self) -> None:
@ -801,13 +653,9 @@ class Updater(Generic[CCT, UD, CD, BD]):
# https://bugs.python.org/issue28206
signal.Signals(signum), # pylint: disable=no-member
)
if self.persistence:
# Update user_data, chat_data and bot_data before flushing
self.dispatcher.update_persistence()
self.persistence.flush()
self.stop()
if self.user_sig_handler:
self.user_sig_handler(signum, frame)
if self.user_signal_handler:
self.user_signal_handler(signum, frame)
else:
self.logger.warning('Exiting immediately!')
# pylint: disable=import-outside-toplevel, protected-access

View file

@ -0,0 +1,61 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2021
# 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/].
"""This module contains helper functions related to inspecting the program stack.
.. versionadded:: 14.0
Warning:
Contents of this module are intended to be used internally by the library and *not* by the
user. Changes to this module are not considered breaking changes and may not be documented in
the changelog.
"""
from pathlib import Path
from types import FrameType
from typing import Optional
def was_called_by(frame: Optional[FrameType], caller: Path) -> bool:
"""Checks if the passed frame was called by the specified file.
Example:
.. code:: python
>>> was_called_by(inspect.currentframe(), Path(__file__))
True
Arguments:
frame (:obj:`FrameType`): The frame - usually the return value of
``inspect.currentframe()``. If :obj:`None` is passed, the return value will be
:obj:`False`.
caller (:obj:`pathlib.Path`): File that should be the caller.
Returns:
:obj:`bool`: Whether or not the frame was called by the specified file.
"""
if frame is None:
return False
# https://stackoverflow.com/a/57712700/10606962
if Path(frame.f_code.co_filename) == caller:
return True
while frame.f_back:
frame = frame.f_back
if Path(frame.f_code.co_filename) == caller:
return True
return False

View file

@ -25,10 +25,11 @@ Warning:
user. Changes to this module are not considered breaking changes and may not be documented in
the changelog.
"""
from typing import TypeVar, TYPE_CHECKING, Tuple, List, Dict, Any, Optional
from typing import TypeVar, TYPE_CHECKING, Tuple, List, Dict, Any, Optional, Union
if TYPE_CHECKING:
from telegram.ext import CallbackContext # noqa: F401
from telegram.ext import CallbackContext, JobQueue, BasePersistence # noqa: F401
from telegram import Bot
ConversationDict = Dict[Tuple[int, ...], Optional[object]]
@ -50,6 +51,11 @@ CCT = TypeVar('CCT', bound='CallbackContext')
.. versionadded:: 13.6
"""
BT = TypeVar('BT', bound='Bot')
"""Type of the bot.
.. versionadded:: 14.0
"""
UD = TypeVar('UD')
"""Type of the user data for a single user.
@ -65,3 +71,11 @@ BD = TypeVar('BD')
.. versionadded:: 13.6
"""
JQ = TypeVar('JQ', bound=Union[None, 'JobQueue'])
"""Type of the job queue.
.. versionadded:: 14.0"""
PT = TypeVar('PT', bound=Union[None, 'BasePersistence'])
"""Type of the persistence.
.. versionadded:: 14.0"""

View file

@ -36,15 +36,15 @@ from typing import Any, Union
import certifi
try:
import telegram.vendor.ptb_urllib3.urllib3 as urllib3
import telegram.vendor.ptb_urllib3.urllib3.contrib.appengine as appengine
from telegram.vendor.ptb_urllib3 import urllib3
from telegram.vendor.ptb_urllib3.urllib3.contrib import appengine
from telegram.vendor.ptb_urllib3.urllib3.connection import HTTPConnection
from telegram.vendor.ptb_urllib3.urllib3.fields import RequestField
from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout
except ImportError: # pragma: no cover
try:
import urllib3 # type: ignore[no-redef]
import urllib3.contrib.appengine as appengine # type: ignore[no-redef]
from urllib3.contrib import appengine # type: ignore[no-redef]
from urllib3.connection import HTTPConnection # type: ignore[no-redef]
from urllib3.fields import RequestField # type: ignore[no-redef]
from urllib3.util.timeout import Timeout # type: ignore[no-redef]

View file

@ -53,12 +53,12 @@ from telegram import (
)
from telegram.ext import (
Dispatcher,
JobQueue,
Updater,
MessageFilter,
Defaults,
UpdateFilter,
ExtBot,
DispatcherBuilder,
UpdaterBuilder,
)
from telegram.error import BadRequest
from telegram.utils.defaultvalue import DefaultValue, DEFAULT_NONE
@ -173,8 +173,7 @@ def provider_token(bot_info):
def create_dp(bot):
# Dispatcher is heavy to init (due to many threads and such) so we have a single session
# scoped one here, but before each test, reset it (dp fixture below)
dispatcher = DictDispatcher(bot, Queue(), job_queue=JobQueue(), workers=2)
dispatcher.job_queue.set_dispatcher(dispatcher)
dispatcher = DispatcherBuilder().bot(bot).workers(2).dispatcher_class(DictDispatcher).build()
thr = Thread(target=dispatcher.start)
thr.start()
sleep(2)
@ -202,7 +201,7 @@ def dp(_dp):
_dp.handlers = {}
_dp.groups = []
_dp.error_handlers = {}
_dp.__exception_event = Event()
_dp.exception_event = Event()
_dp.__stop_event = Event()
_dp.__async_queue = Queue()
_dp.__async_threads = set()
@ -212,7 +211,7 @@ def dp(_dp):
@pytest.fixture(scope='function')
def updater(bot):
up = Updater(bot=bot, workers=2)
up = UpdaterBuilder().bot(bot).workers(2).build()
yield up
if up.running:
up.stop()

View file

@ -184,7 +184,7 @@ class TestBot:
@flaky(3, 1)
def test_invalid_token_server_response(self, monkeypatch):
monkeypatch.setattr('telegram.Bot._validate_token', lambda x, y: True)
monkeypatch.setattr('telegram.Bot._validate_token', lambda x, y: '')
bot = Bot('12')
with pytest.raises(InvalidToken):
bot.get_me()

252
tests/test_builders.py Normal file
View file

@ -0,0 +1,252 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2021
# 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/].
"""
We mainly test on UpdaterBuilder because it has all methods that DispatcherBuilder already has
"""
from random import randint
from threading import Event
import pytest
from telegram.request import Request
from .conftest import PRIVATE_KEY
from telegram.ext import (
UpdaterBuilder,
Defaults,
JobQueue,
PicklePersistence,
ContextTypes,
Dispatcher,
Updater,
)
from telegram.ext.builders import _BOT_CHECKS, _DISPATCHER_CHECKS, DispatcherBuilder, _BaseBuilder
@pytest.fixture(
scope='function',
params=[{'class': UpdaterBuilder}, {'class': DispatcherBuilder}],
ids=['UpdaterBuilder', 'DispatcherBuilder'],
)
def builder(request):
return request.param['class']()
class TestBuilder:
@pytest.mark.parametrize('workers', [randint(1, 100) for _ in range(10)])
def test_get_connection_pool_size(self, workers):
assert _BaseBuilder._get_connection_pool_size(workers) == workers + 4
@pytest.mark.parametrize(
'method, description', _BOT_CHECKS, ids=[entry[0] for entry in _BOT_CHECKS]
)
def test_mutually_exclusive_for_bot(self, builder, method, description):
if getattr(builder, method, None) is None:
pytest.skip(f'{builder.__class__} has no method called {method}')
# First that e.g. `bot` can't be set if `request` was already set
getattr(builder, method)(1)
with pytest.raises(RuntimeError, match=f'`bot` may only be set, if no {description}'):
builder.bot(None)
# Now test that `request` can't be set if `bot` was already set
builder = builder.__class__()
builder.bot(None)
with pytest.raises(RuntimeError, match=f'`{method}` may only be set, if no bot instance'):
getattr(builder, method)(None)
@pytest.mark.parametrize(
'method, description', _DISPATCHER_CHECKS, ids=[entry[0] for entry in _DISPATCHER_CHECKS]
)
def test_mutually_exclusive_for_dispatcher(self, builder, method, description):
if isinstance(builder, DispatcherBuilder):
pytest.skip('This test is only relevant for UpdaterBuilder')
if getattr(builder, method, None) is None:
pytest.skip(f'{builder.__class__} has no method called {method}')
# First that e.g. `dispatcher` can't be set if `bot` was already set
getattr(builder, method)(None)
with pytest.raises(
RuntimeError, match=f'`dispatcher` may only be set, if no {description}'
):
builder.dispatcher(None)
# Now test that `bot` can't be set if `dispatcher` was already set
builder = builder.__class__()
builder.dispatcher(1)
with pytest.raises(
RuntimeError, match=f'`{method}` may only be set, if no Dispatcher instance'
):
getattr(builder, method)(None)
# Finally test that `bot` *can* be set if `dispatcher` was set to None
builder = builder.__class__()
builder.dispatcher(None)
if method != 'dispatcher_class':
getattr(builder, method)(None)
else:
with pytest.raises(
RuntimeError, match=f'`{method}` may only be set, if no Dispatcher instance'
):
getattr(builder, method)(None)
def test_mutually_exclusive_for_request(self, builder):
builder.request(None)
with pytest.raises(
RuntimeError, match='`request_kwargs` may only be set, if no Request instance'
):
builder.request_kwargs(None)
builder = builder.__class__()
builder.request_kwargs(None)
with pytest.raises(RuntimeError, match='`request` may only be set, if no request_kwargs'):
builder.request(None)
def test_build_without_token(self, builder):
with pytest.raises(RuntimeError, match='No bot token was set.'):
builder.build()
def test_build_custom_bot(self, builder, bot):
builder.bot(bot)
obj = builder.build()
assert obj.bot is bot
if isinstance(obj, Updater):
assert obj.dispatcher.bot is bot
assert obj.dispatcher.job_queue.dispatcher is obj.dispatcher
assert obj.exception_event is obj.dispatcher.exception_event
def test_build_custom_dispatcher(self, dp):
updater = UpdaterBuilder().dispatcher(dp).build()
assert updater.dispatcher is dp
assert updater.bot is updater.dispatcher.bot
assert updater.exception_event is dp.exception_event
def test_build_no_dispatcher(self, bot):
updater = UpdaterBuilder().dispatcher(None).token(bot.token).build()
assert updater.dispatcher is None
assert updater.bot.token == bot.token
assert updater.bot.request.con_pool_size == 8
assert isinstance(updater.exception_event, Event)
def test_all_bot_args_custom(self, builder, bot):
defaults = Defaults()
request = Request(8)
builder.token(bot.token).base_url('base_url').base_file_url('base_file_url').private_key(
PRIVATE_KEY
).defaults(defaults).arbitrary_callback_data(42).request(request)
built_bot = builder.build().bot
assert built_bot.token == bot.token
assert built_bot.base_url == 'base_url' + bot.token
assert built_bot.base_file_url == 'base_file_url' + bot.token
assert built_bot.defaults is defaults
assert built_bot.request is request
assert built_bot.callback_data_cache.maxsize == 42
builder = builder.__class__()
builder.token(bot.token).request_kwargs({'connect_timeout': 42})
built_bot = builder.build().bot
assert built_bot.token == bot.token
assert built_bot.request._connect_timeout == 42
def test_all_dispatcher_args_custom(self, dp):
builder = DispatcherBuilder()
job_queue = JobQueue()
persistence = PicklePersistence('filename')
context_types = ContextTypes()
builder.bot(dp.bot).update_queue(dp.update_queue).exception_event(
dp.exception_event
).job_queue(job_queue).persistence(persistence).context_types(context_types).workers(3)
dispatcher = builder.build()
assert dispatcher.bot is dp.bot
assert dispatcher.update_queue is dp.update_queue
assert dispatcher.exception_event is dp.exception_event
assert dispatcher.job_queue is job_queue
assert dispatcher.job_queue.dispatcher is dispatcher
assert dispatcher.persistence is persistence
assert dispatcher.context_types is context_types
assert dispatcher.workers == 3
def test_all_updater_args_custom(self, dp):
updater = (
UpdaterBuilder()
.dispatcher(None)
.bot(dp.bot)
.exception_event(dp.exception_event)
.update_queue(dp.update_queue)
.user_signal_handler(42)
.build()
)
assert updater.dispatcher is None
assert updater.bot is dp.bot
assert updater.exception_event is dp.exception_event
assert updater.update_queue is dp.update_queue
assert updater.user_signal_handler == 42
def test_connection_pool_size_with_workers(self, bot, builder):
obj = builder.token(bot.token).workers(42).build()
dispatcher = obj if isinstance(obj, Dispatcher) else obj.dispatcher
assert dispatcher.workers == 42
assert dispatcher.bot.request.con_pool_size == 46
def test_connection_pool_size_warning(self, bot, builder, recwarn):
builder.token(bot.token).workers(42).request_kwargs({'con_pool_size': 1})
obj = builder.build()
dispatcher = obj if isinstance(obj, Dispatcher) else obj.dispatcher
assert dispatcher.workers == 42
assert dispatcher.bot.request.con_pool_size == 1
assert len(recwarn) == 1
message = str(recwarn[-1].message)
assert 'smaller (1)' in message
assert 'recommended value of 46.' in message
assert recwarn[-1].filename == __file__, "wrong stacklevel"
def test_custom_classes(self, bot, builder):
class CustomDispatcher(Dispatcher):
def __init__(self, arg, **kwargs):
super().__init__(**kwargs)
self.arg = arg
class CustomUpdater(Updater):
def __init__(self, arg, **kwargs):
super().__init__(**kwargs)
self.arg = arg
builder.dispatcher_class(CustomDispatcher, kwargs={'arg': 2}).token(bot.token)
if isinstance(builder, UpdaterBuilder):
builder.updater_class(CustomUpdater, kwargs={'arg': 1})
obj = builder.build()
if isinstance(builder, UpdaterBuilder):
assert isinstance(obj, CustomUpdater)
assert obj.arg == 1
assert isinstance(obj.dispatcher, CustomDispatcher)
assert obj.dispatcher.arg == 2
else:
assert isinstance(obj, CustomDispatcher)
assert obj.arg == 2

View file

@ -33,6 +33,8 @@ from telegram.ext import (
JobQueue,
BasePersistence,
ContextTypes,
DispatcherBuilder,
UpdaterBuilder,
)
from telegram.ext import PersistenceInput
from telegram.ext.dispatcher import Dispatcher, DispatcherHandlerStop
@ -98,14 +100,36 @@ class TestDispatcher:
self.received = context.error.message
def test_slot_behaviour(self, bot, mro_slots):
dp = Dispatcher(bot=bot, update_queue=None)
dp = DispatcherBuilder().bot(bot).build()
for at in dp.__slots__:
at = f"_Dispatcher{at}" if at.startswith('__') and not at.endswith('__') else at
assert getattr(dp, at, 'err') != 'err', f"got extra slot '{at}'"
assert len(mro_slots(dp)) == len(set(mro_slots(dp))), "duplicate slot"
def test_less_than_one_worker_warning(self, dp, recwarn):
Dispatcher(dp.bot, dp.update_queue, job_queue=dp.job_queue, workers=0)
def test_manual_init_warning(self, recwarn):
Dispatcher(
bot=None,
update_queue=None,
workers=7,
exception_event=None,
job_queue=None,
persistence=None,
context_types=ContextTypes(),
)
assert len(recwarn) == 1
assert (
str(recwarn[-1].message)
== '`Dispatcher` instances should be built via the `DispatcherBuilder`.'
)
assert recwarn[0].filename == __file__, "stacklevel is incorrect!"
@pytest.mark.parametrize(
'builder',
(DispatcherBuilder(), UpdaterBuilder()),
ids=('DispatcherBuilder', 'UpdaterBuilder'),
)
def test_less_than_one_worker_warning(self, dp, recwarn, builder):
builder.bot(dp.bot).workers(0).build()
assert len(recwarn) == 1
assert (
str(recwarn[0].message)
@ -113,6 +137,18 @@ class TestDispatcher:
)
assert recwarn[0].filename == __file__, "stacklevel is incorrect!"
def test_builder(self, dp):
builder_1 = dp.builder()
builder_2 = dp.builder()
assert isinstance(builder_1, DispatcherBuilder)
assert isinstance(builder_2, DispatcherBuilder)
assert builder_1 is not builder_2
# Make sure that setting a token doesn't raise an exception
# i.e. check that the builders are "empty"/new
builder_1.token(dp.bot.token)
builder_2.token(dp.bot.token)
def test_one_context_per_update(self, dp):
def one(update, context):
if update.message.text == 'test':
@ -163,7 +199,7 @@ class TestDispatcher:
with pytest.raises(
TypeError, match='persistence must be based on telegram.ext.BasePersistence'
):
Dispatcher(bot, None, persistence=my_per())
DispatcherBuilder().bot(bot).persistence(my_per()).build()
def test_error_handler_that_raises_errors(self, dp):
"""
@ -580,7 +616,7 @@ class TestDispatcher:
),
)
my_persistence = OwnPersistence()
dp = Dispatcher(bot, None, persistence=my_persistence)
dp = DispatcherBuilder().bot(bot).persistence(my_persistence).build()
dp.add_handler(CommandHandler('start', start1))
dp.add_error_handler(error)
dp.process_update(update)
@ -885,7 +921,7 @@ class TestDispatcher:
bot_data=complex,
)
dispatcher = Dispatcher(bot, Queue(), context_types=cc)
dispatcher = DispatcherBuilder().bot(bot).context_types(cc).build()
assert isinstance(dispatcher.user_data[1], int)
assert isinstance(dispatcher.chat_data[1], float)
@ -900,12 +936,15 @@ class TestDispatcher:
type(context.bot_data),
)
dispatcher = Dispatcher(
bot,
Queue(),
context_types=ContextTypes(
context=CustomContext, bot_data=int, user_data=float, chat_data=complex
),
dispatcher = (
DispatcherBuilder()
.bot(bot)
.context_types(
ContextTypes(
context=CustomContext, bot_data=int, user_data=float, chat_data=complex
)
)
.build()
)
dispatcher.add_error_handler(error_handler)
dispatcher.add_handler(MessageHandler(Filters.all, self.callback_raise_error))
@ -923,12 +962,15 @@ class TestDispatcher:
type(context.bot_data),
)
dispatcher = Dispatcher(
bot,
Queue(),
context_types=ContextTypes(
context=CustomContext, bot_data=int, user_data=float, chat_data=complex
),
dispatcher = (
DispatcherBuilder()
.bot(bot)
.context_types(
ContextTypes(
context=CustomContext, bot_data=int, user_data=float, chat_data=complex
)
)
.build()
)
dispatcher.add_handler(MessageHandler(Filters.all, callback))

View file

@ -29,7 +29,13 @@ import pytest
import pytz
from apscheduler.schedulers import SchedulerNotRunningError
from flaky import flaky
from telegram.ext import JobQueue, Updater, Job, CallbackContext, Dispatcher, ContextTypes
from telegram.ext import (
JobQueue,
Job,
CallbackContext,
ContextTypes,
DispatcherBuilder,
)
class CustomContext(CallbackContext):
@ -55,11 +61,6 @@ class TestJobQueue:
job_time = 0
received_error = None
def test_slot_behaviour(self, job_queue, mro_slots, _dp):
for attr in job_queue.__slots__:
assert getattr(job_queue, attr, 'err') != 'err', f"got extra slot '{attr}'"
assert len(mro_slots(job_queue)) == len(set(mro_slots(job_queue))), "duplicate slot"
@pytest.fixture(autouse=True)
def reset(self):
self.result = 0
@ -100,6 +101,22 @@ class TestJobQueue:
def error_handler_raise_error(self, *args):
raise Exception('Failing bigly')
def test_slot_behaviour(self, job_queue, mro_slots, _dp):
for attr in job_queue.__slots__:
assert getattr(job_queue, attr, 'err') != 'err', f"got extra slot '{attr}'"
assert len(mro_slots(job_queue)) == len(set(mro_slots(job_queue))), "duplicate slot"
def test_dispatcher_weakref(self, bot):
jq = JobQueue()
dispatcher = DispatcherBuilder().bot(bot).job_queue(None).build()
with pytest.raises(RuntimeError, match='No dispatcher was set'):
jq.dispatcher
jq.set_dispatcher(dispatcher)
assert jq.dispatcher is dispatcher
del dispatcher
with pytest.raises(RuntimeError, match='no longer alive'):
jq.dispatcher
def test_run_once(self, job_queue):
job_queue.run_once(self.job_run_once, 0.01)
sleep(0.02)
@ -228,19 +245,19 @@ class TestJobQueue:
sleep(0.03)
assert self.result == 1
def test_in_updater(self, bot):
u = Updater(bot=bot)
u.job_queue.start()
def test_in_dispatcher(self, bot):
dispatcher = DispatcherBuilder().bot(bot).build()
dispatcher.job_queue.start()
try:
u.job_queue.run_repeating(self.job_run_once, 0.02)
dispatcher.job_queue.run_repeating(self.job_run_once, 0.02)
sleep(0.03)
assert self.result == 1
u.stop()
dispatcher.stop()
sleep(1)
assert self.result == 1
finally:
try:
u.stop()
dispatcher.stop()
except SchedulerNotRunningError:
pass
@ -479,12 +496,15 @@ class TestJobQueue:
assert 'No error handlers are registered' in rec.getMessage()
def test_custom_context(self, bot, job_queue):
dispatcher = Dispatcher(
bot,
Queue(),
context_types=ContextTypes(
context=CustomContext, bot_data=int, user_data=float, chat_data=complex
),
dispatcher = (
DispatcherBuilder()
.bot(bot)
.context_types(
ContextTypes(
context=CustomContext, bot_data=int, user_data=float, chat_data=complex
)
)
.build()
)
job_queue.set_dispatcher(dispatcher)

View file

@ -22,7 +22,7 @@ import uuid
from pathlib import Path
from threading import Lock
from telegram.ext import PersistenceInput
from telegram.ext import PersistenceInput, UpdaterBuilder
from telegram.ext.callbackdatacache import CallbackDataCache
try:
@ -41,7 +41,6 @@ import pytest
from telegram import Update, Message, User, Chat, MessageEntity, Bot
from telegram.ext import (
BasePersistence,
Updater,
ConversationHandler,
MessageHandler,
Filters,
@ -215,7 +214,7 @@ def conversations():
@pytest.fixture(scope="function")
def updater(bot, base_persistence):
base_persistence.store_data = PersistenceInput(False, False, False, False)
u = Updater(bot=bot, persistence=base_persistence)
u = UpdaterBuilder().bot(bot).persistence(base_persistence).build()
base_persistence.store_data = PersistenceInput()
return u
@ -304,34 +303,36 @@ class TestBasePersistence:
base_persistence.get_callback_data = get_callback_data
with pytest.raises(ValueError, match="user_data must be of type defaultdict"):
u = Updater(bot=bot, persistence=base_persistence)
UpdaterBuilder().bot(bot).persistence(base_persistence).build()
def get_user_data():
return user_data
base_persistence.get_user_data = get_user_data
with pytest.raises(ValueError, match="chat_data must be of type defaultdict"):
Updater(bot=bot, persistence=base_persistence)
UpdaterBuilder().bot(bot).persistence(base_persistence).build()
def get_chat_data():
return chat_data
base_persistence.get_chat_data = get_chat_data
with pytest.raises(ValueError, match="bot_data must be of type dict"):
Updater(bot=bot, persistence=base_persistence)
UpdaterBuilder().bot(bot).persistence(base_persistence).build()
def get_bot_data():
return bot_data
base_persistence.get_bot_data = get_bot_data
with pytest.raises(ValueError, match="callback_data must be a 2-tuple"):
Updater(bot=bot, persistence=base_persistence)
UpdaterBuilder().bot(bot).persistence(base_persistence).build()
def get_callback_data():
return callback_data
base_persistence.bot = None
base_persistence.get_callback_data = get_callback_data
u = Updater(bot=bot, persistence=base_persistence)
u = UpdaterBuilder().bot(bot).persistence(base_persistence).build()
assert u.dispatcher.bot is base_persistence.bot
assert u.dispatcher.bot_data == bot_data
assert u.dispatcher.chat_data == chat_data
assert u.dispatcher.user_data == user_data
@ -373,7 +374,7 @@ class TestBasePersistence:
base_persistence.refresh_bot_data = lambda x: x
base_persistence.refresh_chat_data = lambda x, y: x
base_persistence.refresh_user_data = lambda x, y: x
updater = Updater(bot=bot, persistence=base_persistence)
updater = UpdaterBuilder().bot(bot).persistence(base_persistence).build()
dp = updater.dispatcher
def callback_known_user(update, context):
@ -1622,7 +1623,7 @@ class TestPicklePersistence:
assert conversations_test['name1'] == conversation1
def test_with_handler(self, bot, update, bot_data, pickle_persistence, good_pickle_files):
u = Updater(bot=bot, persistence=pickle_persistence)
u = UpdaterBuilder().bot(bot).persistence(pickle_persistence).build()
dp = u.dispatcher
bot.callback_data_cache.clear_callback_data()
bot.callback_data_cache.clear_callback_queries()
@ -1660,13 +1661,13 @@ class TestPicklePersistence:
single_file=False,
on_flush=False,
)
u = Updater(bot=bot, persistence=pickle_persistence_2)
u = UpdaterBuilder().bot(bot).persistence(pickle_persistence_2).build()
dp = u.dispatcher
dp.add_handler(h2)
dp.process_update(update)
def test_flush_on_stop(self, bot, update, pickle_persistence):
u = Updater(bot=bot, persistence=pickle_persistence)
u = UpdaterBuilder().bot(bot).persistence(pickle_persistence).build()
dp = u.dispatcher
u.running = True
dp.user_data[4242424242]['my_test'] = 'Working!'
@ -1686,7 +1687,7 @@ class TestPicklePersistence:
assert data['test'] == 'Working4!'
def test_flush_on_stop_only_bot(self, bot, update, pickle_persistence_only_bot):
u = Updater(bot=bot, persistence=pickle_persistence_only_bot)
u = UpdaterBuilder().bot(bot).persistence(pickle_persistence_only_bot).build()
dp = u.dispatcher
u.running = True
dp.user_data[4242424242]['my_test'] = 'Working!'
@ -1706,7 +1707,7 @@ class TestPicklePersistence:
assert pickle_persistence_2.get_callback_data() is None
def test_flush_on_stop_only_chat(self, bot, update, pickle_persistence_only_chat):
u = Updater(bot=bot, persistence=pickle_persistence_only_chat)
u = UpdaterBuilder().bot(bot).persistence(pickle_persistence_only_chat).build()
dp = u.dispatcher
u.running = True
dp.user_data[4242424242]['my_test'] = 'Working!'
@ -1726,7 +1727,7 @@ class TestPicklePersistence:
assert pickle_persistence_2.get_callback_data() is None
def test_flush_on_stop_only_user(self, bot, update, pickle_persistence_only_user):
u = Updater(bot=bot, persistence=pickle_persistence_only_user)
u = UpdaterBuilder().bot(bot).persistence(pickle_persistence_only_user).build()
dp = u.dispatcher
u.running = True
dp.user_data[4242424242]['my_test'] = 'Working!'
@ -1746,7 +1747,7 @@ class TestPicklePersistence:
assert pickle_persistence_2.get_callback_data() is None
def test_flush_on_stop_only_callback(self, bot, update, pickle_persistence_only_callback):
u = Updater(bot=bot, persistence=pickle_persistence_only_callback)
u = UpdaterBuilder().bot(bot).persistence(pickle_persistence_only_callback).build()
dp = u.dispatcher
u.running = True
dp.user_data[4242424242]['my_test'] = 'Working!'
@ -2194,7 +2195,7 @@ class TestDictPersistence:
def test_with_handler(self, bot, update):
dict_persistence = DictPersistence()
u = Updater(bot=bot, persistence=dict_persistence)
u = UpdaterBuilder().bot(bot).persistence(dict_persistence).build()
dp = u.dispatcher
def first(update, context):
@ -2236,7 +2237,7 @@ class TestDictPersistence:
callback_data_json=callback_data,
)
u = Updater(bot=bot, persistence=dict_persistence_2)
u = UpdaterBuilder().bot(bot).persistence(dict_persistence_2).build()
dp = u.dispatcher
dp.add_handler(h2)
dp.process_update(update)

35
tests/test_stack.py Normal file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2021
# 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 pathlib import Path
from telegram.ext.utils.stack import was_called_by
class TestStack:
def test_none_input(self):
assert not was_called_by(None, None)
def test_called_by_current_file(self):
frame = inspect.currentframe()
file = Path(__file__)
assert was_called_by(frame, file)
# Testing a call by a different file is somewhat hard but it's covered in
# TestUpdater/Dispatcher.test_manual_init_warning

View file

@ -48,14 +48,12 @@ from telegram import (
)
from telegram.error import Unauthorized, InvalidToken, TimedOut, RetryAfter, TelegramError
from telegram.ext import (
Updater,
Dispatcher,
DictPersistence,
Defaults,
InvalidCallbackData,
ExtBot,
Updater,
UpdaterBuilder,
DispatcherBuilder,
)
from telegram.warnings import PTBDeprecationWarning
from telegram.ext.utils.webhookhandler import WebhookServer
signalskip = pytest.mark.skipif(
@ -90,12 +88,6 @@ class TestUpdater:
offset = 0
test_flag = False
def test_slot_behaviour(self, updater, mro_slots):
for at in updater.__slots__:
at = f"_Updater{at}" if at.startswith('__') and not at.endswith('__') else at
assert getattr(updater, at, 'err') != 'err', f"got extra slot '{at}'"
assert len(mro_slots(updater)) == len(set(mro_slots(updater))), "duplicate slot"
@pytest.fixture(autouse=True)
def reset(self):
self.message_count = 0
@ -113,18 +105,49 @@ class TestUpdater:
self.received = update.message.text
self.cb_handler_called.set()
def test_warn_arbitrary_callback_data(self, bot, recwarn):
Updater(bot=bot, arbitrary_callback_data=True)
def test_slot_behaviour(self, updater, mro_slots):
for at in updater.__slots__:
at = f"_Updater{at}" if at.startswith('__') and not at.endswith('__') else at
assert getattr(updater, at, 'err') != 'err', f"got extra slot '{at}'"
assert len(mro_slots(updater)) == len(set(mro_slots(updater))), "duplicate slot"
def test_manual_init_warning(self, recwarn):
Updater(
bot=None,
dispatcher=None,
update_queue=None,
exception_event=None,
user_signal_handler=None,
)
assert len(recwarn) == 1
assert 'Passing arbitrary_callback_data to an Updater' in str(recwarn[0].message)
assert (
str(recwarn[-1].message)
== '`Updater` instances should be built via the `UpdaterBuilder`.'
)
assert recwarn[0].filename == __file__, "stacklevel is incorrect!"
def test_builder(self, updater):
builder_1 = updater.builder()
builder_2 = updater.builder()
assert isinstance(builder_1, UpdaterBuilder)
assert isinstance(builder_2, UpdaterBuilder)
assert builder_1 is not builder_2
# Make sure that setting a token doesn't raise an exception
# i.e. check that the builders are "empty"/new
builder_1.token(updater.bot.token)
builder_2.token(updater.bot.token)
def test_warn_con_pool(self, bot, recwarn, dp):
dp = Dispatcher(bot, Queue(), workers=5)
Updater(bot=bot, workers=8)
Updater(dispatcher=dp, workers=None)
DispatcherBuilder().bot(bot).workers(5).build()
UpdaterBuilder().bot(bot).workers(8).build()
UpdaterBuilder().bot(bot).workers(2).build()
assert len(recwarn) == 2
for idx, value in enumerate((12, 9)):
warning = f'Connection pool of Request object is smaller than optimal value {value}'
for idx, value in enumerate((9, 12)):
warning = (
'The Connection pool of Request object is smaller (8) than the '
f'recommended value of {value}.'
)
assert str(recwarn[idx].message) == warning
assert recwarn[idx].filename == __file__, "wrong stacklevel!"
@ -305,9 +328,21 @@ class TestUpdater:
updater.bot.callback_data_cache.clear_callback_data()
updater.bot.callback_data_cache.clear_callback_queries()
def test_start_webhook_no_warning_or_error_logs(self, caplog, updater, monkeypatch):
@pytest.mark.parametrize('use_dispatcher', (True, False))
def test_start_webhook_no_warning_or_error_logs(
self, caplog, updater, monkeypatch, use_dispatcher
):
if not use_dispatcher:
updater.dispatcher = None
self.test_flag = 0
def set_flag():
self.test_flag += 1
monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True)
monkeypatch.setattr(updater.bot._request, 'stop', lambda *args, **kwargs: set_flag())
# prevent api calls from @info decorator when updater.bot.id is used in thread names
monkeypatch.setattr(updater.bot, '_bot', User(id=123, first_name='bot', is_bot=True))
@ -317,6 +352,8 @@ class TestUpdater:
updater.start_webhook(ip, port)
updater.stop()
assert not caplog.records
# Make sure that bot.request.stop() has been called exactly once
assert self.test_flag == 1
def test_webhook_ssl(self, monkeypatch, updater):
monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
@ -606,7 +643,7 @@ class TestUpdater:
def user_signal_inc(signum, frame):
temp_var['a'] = 1
updater.user_sig_handler = user_signal_inc
updater.user_signal_handler = user_signal_inc
updater.start_polling(0.01)
Thread(target=partial(self.signal_sender, updater=updater)).start()
updater.idle()
@ -614,47 +651,3 @@ class TestUpdater:
sleep(0.5)
assert updater.running is False
assert temp_var['a'] != 0
def test_create_bot(self):
updater = Updater('123:abcd')
assert updater.bot is not None
def test_mutual_exclude_token_bot(self):
bot = Bot('123:zyxw')
with pytest.raises(ValueError):
Updater(token='123:abcd', bot=bot)
def test_no_token_or_bot_or_dispatcher(self):
with pytest.raises(ValueError):
Updater()
def test_mutual_exclude_bot_private_key(self):
bot = Bot('123:zyxw')
with pytest.raises(ValueError):
Updater(bot=bot, private_key=b'key')
def test_mutual_exclude_bot_dispatcher(self, bot):
dispatcher = Dispatcher(bot, None)
bot = Bot('123:zyxw')
with pytest.raises(ValueError):
Updater(bot=bot, dispatcher=dispatcher)
def test_mutual_exclude_persistence_dispatcher(self, bot):
dispatcher = Dispatcher(bot, None)
persistence = DictPersistence()
with pytest.raises(ValueError):
Updater(dispatcher=dispatcher, persistence=persistence)
def test_mutual_exclude_workers_dispatcher(self, bot):
dispatcher = Dispatcher(bot, None)
with pytest.raises(ValueError):
Updater(dispatcher=dispatcher, workers=8)
def test_mutual_exclude_custom_context_dispatcher(self):
dispatcher = Dispatcher(None, None)
with pytest.raises(ValueError):
Updater(dispatcher=dispatcher, context_types=True)
def test_defaults_warning(self, bot):
with pytest.warns(PTBDeprecationWarning, match='no effect when a Bot is passed'):
Updater(bot=bot, defaults=Defaults())