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: hooks:
- id: flake8 - id: flake8
- repo: https://github.com/PyCQA/pylint - repo: https://github.com/PyCQA/pylint
rev: v2.8.3 rev: v2.10.2
hooks: hooks:
- id: pylint - id: pylint
files: ^(telegram|examples)/.*\.py$ files: ^(telegram|examples)/.*\.py$
@ -27,12 +27,17 @@ repos:
- cachetools==4.2.2 - cachetools==4.2.2
- . # this basically does `pip install -e .` - . # this basically does `pip install -e .`
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.812 rev: v0.910
hooks: hooks:
- id: mypy - id: mypy
name: mypy-ptb name: mypy-ptb
files: ^telegram/.*\.py$ files: ^telegram/.*\.py$
additional_dependencies: additional_dependencies:
- types-ujson
- types-pytz
- types-cryptography
- types-certifi
- types-cachetools
- certifi - certifi
- tornado>=6.1 - tornado>=6.1
- APScheduler==3.6.3 - APScheduler==3.6.3
@ -51,7 +56,7 @@ repos:
- cachetools==4.2.2 - cachetools==4.2.2
- . # this basically does `pip install -e .` - . # this basically does `pip install -e .`
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.19.1 rev: v2.24.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
files: ^(telegram|examples|tests)/.*\.py$ 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:: .. toctree::
telegram.ext.extbot telegram.ext.extbot
telegram.ext.updaterbuilder
telegram.ext.updater telegram.ext.updater
telegram.ext.dispatcherbuilder
telegram.ext.dispatcher telegram.ext.dispatcher
telegram.ext.dispatcherhandlerstop telegram.ext.dispatcherhandlerstop
telegram.ext.callbackcontext telegram.ext.callbackcontext
@ -61,4 +63,5 @@ utils
.. toctree:: .. toctree::
telegram.ext.utils.promise telegram.ext.utils.promise
telegram.ext.utils.stack
telegram.ext.utils.types 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 import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import ( from telegram.ext import (
Updater,
CommandHandler, CommandHandler,
CallbackQueryHandler, CallbackQueryHandler,
CallbackContext,
InvalidCallbackData, InvalidCallbackData,
PicklePersistence, PicklePersistence,
Updater,
CallbackContext,
) )
# Enable logging
logging.basicConfig( logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
) )
logger = logging.getLogger(__name__) 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.""" """Sends a message with 5 inline buttons attached."""
number_list: List[int] = [] number_list: List[int] = []
update.message.reply_text('Please choose:', reply_markup=build_keyboard(number_list)) 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.""" """Displays info on how to use the bot."""
update.message.reply_text( update.message.reply_text(
"Use /start to test this bot. Use /clear to clear the stored data so that you can see " "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""" """Clears the callback data cache"""
context.bot.callback_data_cache.clear_callback_data() # type: ignore[attr-defined] context.bot.callback_data_cache.clear_callback_data()
context.bot.callback_data_cache.clear_callback_queries() # type: ignore[attr-defined] context.bot.callback_data_cache.clear_callback_queries()
update.effective_message.reply_text('All clear!') 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.""" """Parses the CallbackQuery and updates the message text."""
query = update.callback_query query = update.callback_query
query.answer() query.answer()
@ -73,7 +75,7 @@ def list_button(update: Update, context: CallbackContext) -> None:
context.drop_callback_data(query) 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.""" """Informs the user that the button is no longer available."""
update.callback_query.answer() update.callback_query.answer()
update.effective_message.edit_text( 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 # We use persistence to demonstrate how buttons can still work after the bot was restarted
persistence = PicklePersistence(filepath='arbitrarycallbackdatabot') persistence = PicklePersistence(filepath='arbitrarycallbackdatabot')
# Create the Updater and pass it your bot's token. # 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('start', start))
updater.dispatcher.add_handler(CommandHandler('help', help_command)) 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 import Update, Chat, ChatMember, ParseMode, ChatMemberUpdated
from telegram.ext import ( from telegram.ext import (
Updater,
CommandHandler, CommandHandler,
CallbackContext,
ChatMemberHandler, ChatMemberHandler,
Updater,
CallbackContext,
) )
# Enable logging # Enable logging
logging.basicConfig( logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO 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 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.""" """Tracks the chats the bot is in."""
result = extract_status_change(update.my_chat_member) result = extract_status_change(update.my_chat_member)
if result is None: 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) 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""" """Shows which chats the bot is in"""
user_ids = ", ".join(str(uid) for uid in context.bot_data.setdefault("user_ids", set())) 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())) 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) 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""" """Greets new users in chats and announces when someone leaves"""
result = extract_status_change(update.chat_member) result = extract_status_change(update.chat_member)
if result is None: if result is None:
@ -139,7 +140,7 @@ def greet_chat_members(update: Update, context: CallbackContext) -> None:
def main() -> None: def main() -> None:
"""Start the bot.""" """Start the bot."""
# Create the Updater and pass it your bot's token. # 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 # Get the dispatcher to register handlers
dispatcher = updater.dispatcher dispatcher = updater.dispatcher

View file

@ -15,13 +15,14 @@ from typing import DefaultDict, Optional, Set
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ParseMode from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ParseMode
from telegram.ext import ( from telegram.ext import (
Updater,
CommandHandler, CommandHandler,
CallbackContext, CallbackContext,
ContextTypes, ContextTypes,
CallbackQueryHandler, CallbackQueryHandler,
TypeHandler, TypeHandler,
Dispatcher, Dispatcher,
ExtBot,
Updater,
) )
@ -32,8 +33,8 @@ class ChatData:
self.clicks_per_message: DefaultDict[int, int] = defaultdict(int) self.clicks_per_message: DefaultDict[int, int] = defaultdict(int)
# The [dict, ChatData, dict] is for type checkers like mypy # The [ExtBot, dict, ChatData, dict] is for type checkers like mypy
class CustomContext(CallbackContext[dict, ChatData, dict]): class CustomContext(CallbackContext[ExtBot, dict, ChatData, dict]):
"""Custom class for context.""" """Custom class for context."""
def __init__(self, dispatcher: Dispatcher): def __init__(self, dispatcher: Dispatcher):
@ -113,7 +114,7 @@ def track_users(update: Update, context: CustomContext) -> None:
def main() -> None: def main() -> None:
"""Run the bot.""" """Run the bot."""
context_types = ContextTypes(context=CustomContext, chat_data=ChatData) 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 dispatcher = updater.dispatcher
# run track_users in its own group to not interfere with the user handlers # 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 import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
from telegram.ext import ( from telegram.ext import (
Updater,
CommandHandler, CommandHandler,
MessageHandler, MessageHandler,
Filters, Filters,
ConversationHandler, ConversationHandler,
Updater,
CallbackContext, CallbackContext,
) )
# Enable logging # Enable logging
logging.basicConfig( logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
GENDER, PHOTO, LOCATION, BIO = range(4) 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.""" """Starts the conversation and asks the user about their gender."""
reply_keyboard = [['Boy', 'Girl', 'Other']] reply_keyboard = [['Boy', 'Girl', 'Other']]
@ -52,7 +52,7 @@ def start(update: Update, context: CallbackContext) -> int:
return GENDER 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.""" """Stores the selected gender and asks for a photo."""
user = update.message.from_user user = update.message.from_user
logger.info("Gender of %s: %s", user.first_name, update.message.text) 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 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.""" """Stores the photo and asks for a location."""
user = update.message.from_user user = update.message.from_user
photo_file = update.message.photo[-1].get_file() photo_file = update.message.photo[-1].get_file()
@ -78,7 +78,7 @@ def photo(update: Update, context: CallbackContext) -> int:
return LOCATION 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.""" """Skips the photo and asks for a location."""
user = update.message.from_user user = update.message.from_user
logger.info("User %s did not send a photo.", user.first_name) 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 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.""" """Stores the location and asks for some info about the user."""
user = update.message.from_user user = update.message.from_user
user_location = update.message.location user_location = update.message.location
@ -103,7 +103,7 @@ def location(update: Update, context: CallbackContext) -> int:
return BIO 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.""" """Skips the location and asks for info about the user."""
user = update.message.from_user user = update.message.from_user
logger.info("User %s did not send a location.", user.first_name) 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 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.""" """Stores the info about the user and ends the conversation."""
user = update.message.from_user user = update.message.from_user
logger.info("Bio of %s: %s", user.first_name, update.message.text) 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 return ConversationHandler.END
def cancel(update: Update, context: CallbackContext) -> int: def cancel(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Cancels and ends the conversation.""" """Cancels and ends the conversation."""
user = update.message.from_user user = update.message.from_user
logger.info("User %s canceled the conversation.", user.first_name) logger.info("User %s canceled the conversation.", user.first_name)
@ -137,7 +137,7 @@ def cancel(update: Update, context: CallbackContext) -> int:
def main() -> None: def main() -> None:
"""Run the bot.""" """Run the bot."""
# Create the Updater and pass it your bot's token. # 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 # Get the dispatcher to register handlers
dispatcher = updater.dispatcher dispatcher = updater.dispatcher

View file

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

View file

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

View file

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

View file

@ -9,12 +9,12 @@ import logging
import traceback import traceback
from telegram import Update, ParseMode from telegram import Update, ParseMode
from telegram.ext import Updater, CallbackContext, CommandHandler from telegram.ext import CommandHandler, Updater, CallbackContext
# Enable logging
logging.basicConfig( logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# The token you got from @botfather when you created the bot # The token you got from @botfather when you created the bot
@ -25,7 +25,7 @@ BOT_TOKEN = "TOKEN"
DEVELOPER_CHAT_ID = 123456789 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 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. # 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) 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) 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.""" """Raise an error to trigger the error handler."""
context.bot.wrong_method_name() # type: ignore[attr-defined] 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.""" """Displays info on how to trigger an error."""
update.effective_message.reply_html( update.effective_message.reply_html(
'Use /bad_command to cause an error.\n' 'Use /bad_command to cause an error.\n'
@ -67,7 +67,7 @@ def start(update: Update, context: CallbackContext) -> None:
def main() -> None: def main() -> None:
"""Run the bot.""" """Run the bot."""
# Create the Updater and pass it your bot's token. # 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 # Get the dispatcher to register handlers
dispatcher = updater.dispatcher dispatcher = updater.dispatcher

View file

@ -23,23 +23,22 @@ from telegram.ext import Updater, InlineQueryHandler, CommandHandler, CallbackCo
logging.basicConfig( logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments update and # Define a few command handlers. These usually take the two arguments update and
# context. Error handlers also receive the raised TelegramError object in error. # 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.""" """Send a message when the command /start is issued."""
update.message.reply_text('Hi!') 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.""" """Send a message when the command /help is issued."""
update.message.reply_text('Help!') 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.""" """Handle the inline query."""
query = update.inline_query.query query = update.inline_query.query
@ -74,7 +73,7 @@ def inlinequery(update: Update, context: CallbackContext) -> None:
def main() -> None: def main() -> None:
"""Run the bot.""" """Run the bot."""
# Create the Updater and pass it your bot's token. # 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 # Get the dispatcher to register handlers
dispatcher = updater.dispatcher 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 import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update 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( logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
) )
logger = logging.getLogger(__name__) 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.""" """Sends a message with three inline buttons attached."""
keyboard = [ keyboard = [
[ [
@ -32,7 +39,7 @@ def start(update: Update, context: CallbackContext) -> None:
update.message.reply_text('Please choose:', reply_markup=reply_markup) 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.""" """Parses the CallbackQuery and updates the message text."""
query = update.callback_query 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}") 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.""" """Displays info on how to use the bot."""
update.message.reply_text("Use /start to test this 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: def main() -> None:
"""Run the bot.""" """Run the bot."""
# Create the Updater and pass it your bot's token. # 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(CommandHandler('start', start))
updater.dispatcher.add_handler(CallbackQueryHandler(button)) 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 import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import ( from telegram.ext import (
Updater,
CommandHandler, CommandHandler,
CallbackQueryHandler, CallbackQueryHandler,
ConversationHandler, ConversationHandler,
Updater,
CallbackContext, CallbackContext,
) )
# Enable logging # Enable logging
logging.basicConfig( logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Stages # Stages
@ -37,7 +37,7 @@ FIRST, SECOND = range(2)
ONE, TWO, THREE, FOUR = range(4) 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`.""" """Send message on `/start`."""
# Get user that sent /start and log his name # Get user that sent /start and log his name
user = update.message.from_user user = update.message.from_user
@ -59,7 +59,7 @@ def start(update: Update, context: CallbackContext) -> int:
return FIRST 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""" """Prompt same text & keyboard as `start` does but not as new message"""
# Get CallbackQuery from Update # Get CallbackQuery from Update
query = update.callback_query query = update.callback_query
@ -80,7 +80,7 @@ def start_over(update: Update, context: CallbackContext) -> int:
return FIRST return FIRST
def one(update: Update, context: CallbackContext) -> int: def one(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Show new choice of buttons""" """Show new choice of buttons"""
query = update.callback_query query = update.callback_query
query.answer() query.answer()
@ -97,7 +97,7 @@ def one(update: Update, context: CallbackContext) -> int:
return FIRST return FIRST
def two(update: Update, context: CallbackContext) -> int: def two(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Show new choice of buttons""" """Show new choice of buttons"""
query = update.callback_query query = update.callback_query
query.answer() query.answer()
@ -114,7 +114,7 @@ def two(update: Update, context: CallbackContext) -> int:
return FIRST return FIRST
def three(update: Update, context: CallbackContext) -> int: def three(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Show new choice of buttons""" """Show new choice of buttons"""
query = update.callback_query query = update.callback_query
query.answer() query.answer()
@ -132,7 +132,7 @@ def three(update: Update, context: CallbackContext) -> int:
return SECOND return SECOND
def four(update: Update, context: CallbackContext) -> int: def four(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Show new choice of buttons""" """Show new choice of buttons"""
query = update.callback_query query = update.callback_query
query.answer() query.answer()
@ -149,7 +149,7 @@ def four(update: Update, context: CallbackContext) -> int:
return FIRST return FIRST
def end(update: Update, context: CallbackContext) -> int: def end(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
"""Returns `ConversationHandler.END`, which tells the """Returns `ConversationHandler.END`, which tells the
ConversationHandler that the conversation is over. ConversationHandler that the conversation is over.
""" """
@ -162,7 +162,7 @@ def end(update: Update, context: CallbackContext) -> int:
def main() -> None: def main() -> None:
"""Run the bot.""" """Run the bot."""
# Create the Updater and pass it your bot's token. # 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 # Get the dispatcher to register handlers
dispatcher = updater.dispatcher dispatcher = updater.dispatcher

View file

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

View file

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

View file

@ -8,24 +8,24 @@ import logging
from telegram import LabeledPrice, ShippingOption, Update from telegram import LabeledPrice, ShippingOption, Update
from telegram.ext import ( from telegram.ext import (
Updater,
CommandHandler, CommandHandler,
MessageHandler, MessageHandler,
Filters, Filters,
PreCheckoutQueryHandler, PreCheckoutQueryHandler,
ShippingQueryHandler, ShippingQueryHandler,
Updater,
CallbackContext, CallbackContext,
) )
# Enable logging # Enable logging
logging.basicConfig( logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
) )
logger = logging.getLogger(__name__) 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.""" """Displays info on how to use the bot."""
msg = ( msg = (
"Use /shipping to get an invoice for shipping-payment, or /noshipping for an " "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) 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.""" """Sends an invoice with shipping-payment."""
chat_id = update.message.chat_id chat_id = update.message.chat_id
title = "Payment Example" 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.""" """Sends an invoice without shipping-payment."""
chat_id = update.message.chat_id chat_id = update.message.chat_id
title = "Payment Example" 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""" """Answers the ShippingQuery with ShippingOptions"""
query = update.shipping_query query = update.shipping_query
# check the payload, is this from your bot? # 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 # 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""" """Answers the PreQecheckoutQuery"""
query = update.pre_checkout_query query = update.pre_checkout_query
# check the payload, is this from your bot? # 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... # 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.""" """Confirms the successful payment."""
# do something after successfully receiving payment? # do something after successfully receiving payment?
update.message.reply_text("Thank you for your 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: def main() -> None:
"""Run the bot.""" """Run the bot."""
# Create the Updater and pass it your bot's token. # 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 # Get the dispatcher to register handlers
dispatcher = updater.dispatcher dispatcher = updater.dispatcher

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -47,6 +47,7 @@ from .chatmemberhandler import ChatMemberHandler
from .chatjoinrequesthandler import ChatJoinRequestHandler from .chatjoinrequesthandler import ChatJoinRequestHandler
from .defaults import Defaults from .defaults import Defaults
from .callbackdatacache import CallbackDataCache, InvalidCallbackData from .callbackdatacache import CallbackDataCache, InvalidCallbackData
from .builders import DispatcherBuilder, UpdaterBuilder
__all__ = ( __all__ = (
'BaseFilter', 'BaseFilter',
@ -63,6 +64,7 @@ __all__ = (
'Defaults', 'Defaults',
'DictPersistence', 'DictPersistence',
'Dispatcher', 'Dispatcher',
'DispatcherBuilder',
'DispatcherHandlerStop', 'DispatcherHandlerStop',
'ExtBot', 'ExtBot',
'Filters', 'Filters',
@ -85,4 +87,5 @@ __all__ = (
'TypeHandler', 'TypeHandler',
'UpdateFilter', 'UpdateFilter',
'Updater', '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 import Update, CallbackQuery
from telegram.ext import ExtBot 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: if TYPE_CHECKING:
from telegram import Bot
from telegram.ext import Dispatcher, Job, JobQueue from telegram.ext import Dispatcher, Job, JobQueue
from telegram.ext.utils.types import CCT 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` 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 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__ = ( __slots__ = (
'_dispatcher', '_dispatcher',
'_chat_id_and_data', '_chat_id_and_data',
@ -107,7 +126,7 @@ class CallbackContext(Generic[UD, CD, BD]):
'__dict__', '__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: Args:
dispatcher (:class:`telegram.ext.Dispatcher`): dispatcher (:class:`telegram.ext.Dispatcher`):
@ -123,7 +142,7 @@ class CallbackContext(Generic[UD, CD, BD]):
self.async_kwargs: Optional[Dict[str, object]] = None self.async_kwargs: Optional[Dict[str, object]] = None
@property @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.""" """:class:`telegram.ext.Dispatcher`: The dispatcher associated with this context."""
return self._dispatcher return self._dispatcher
@ -232,7 +251,7 @@ class CallbackContext(Generic[UD, CD, BD]):
cls: Type['CCT'], cls: Type['CCT'],
update: object, update: object,
error: Exception, error: Exception,
dispatcher: 'Dispatcher[CCT, UD, CD, BD]', dispatcher: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]',
async_args: Union[List, Tuple] = None, async_args: Union[List, Tuple] = None,
async_kwargs: Dict[str, object] = None, async_kwargs: Dict[str, object] = None,
job: 'Job' = None, job: 'Job' = None,
@ -271,7 +290,7 @@ class CallbackContext(Generic[UD, CD, BD]):
@classmethod @classmethod
def from_update( 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': ) -> 'CCT':
""" """
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the 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 return self
@classmethod @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 Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to a
job callback. job callback.
@ -335,7 +356,7 @@ class CallbackContext(Generic[UD, CD, BD]):
setattr(self, key, value) setattr(self, key, value)
@property @property
def bot(self) -> 'Bot': def bot(self) -> BT:
""":class:`telegram.Bot`: The bot associated with this context.""" """:class:`telegram.Bot`: The bot associated with this context."""
return self._dispatcher.bot return self._dispatcher.bot

View file

@ -21,6 +21,7 @@
from typing import Type, Generic, overload, Dict # pylint: disable=unused-import from typing import Type, Generic, overload, Dict # pylint: disable=unused-import
from telegram.ext.callbackcontext import CallbackContext 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 from telegram.ext.utils.types import CCT, UD, CD, BD
@ -54,7 +55,7 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
@overload @overload
def __init__( 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 @overload
def __init__( 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 @overload
def __init__( 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 @overload
def __init__( 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 @overload
def __init__( 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], user_data: Type[UD],
chat_data: Type[CD], chat_data: Type[CD],
): ):
@ -108,7 +112,7 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
@overload @overload
def __init__( 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], user_data: Type[UD],
bot_data: Type[BD], bot_data: Type[BD],
): ):
@ -116,7 +120,7 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
@overload @overload
def __init__( 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], chat_data: Type[CD],
bot_data: Type[BD], bot_data: Type[BD],
): ):
@ -151,7 +155,7 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
@overload @overload
def __init__( 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], user_data: Type[UD],
chat_data: Type[CD], chat_data: Type[CD],
bot_data: Type[BD], bot_data: Type[BD],

View file

@ -23,7 +23,18 @@ import logging
import functools import functools
import datetime import datetime
from threading import Lock 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 import Update
from telegram.ext import ( from telegram.ext import (
@ -41,7 +52,7 @@ from telegram.ext.utils.types import CCT
from telegram.utils.warnings import warn from telegram.utils.warnings import warn
if TYPE_CHECKING: if TYPE_CHECKING:
from telegram.ext import Dispatcher, Job from telegram.ext import Dispatcher, Job, JobQueue
CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]] CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]]
@ -52,7 +63,7 @@ class _ConversationTimeoutContext:
self, self,
conversation_key: Tuple[int, ...], conversation_key: Tuple[int, ...],
update: Update, update: Update,
dispatcher: 'Dispatcher', dispatcher: 'Dispatcher[Any, CCT, Any, Any, Any, JobQueue, Any]',
callback_context: CallbackContext, callback_context: CallbackContext,
): ):
self.conversation_key = conversation_key self.conversation_key = conversation_key
@ -489,7 +500,7 @@ class ConversationHandler(Handler[Update, CCT]):
def _schedule_job( def _schedule_job(
self, self,
new_state: object, new_state: object,
dispatcher: 'Dispatcher', dispatcher: 'Dispatcher[Any, CCT, Any, Any, Any, JobQueue, Any]',
update: Update, update: Update,
context: CallbackContext, context: CallbackContext,
conversation_key: Tuple[int, ...], conversation_key: Tuple[int, ...],
@ -498,7 +509,7 @@ class ConversationHandler(Handler[Update, CCT]):
try: try:
# both job_queue & conversation_timeout are checked before calling _schedule_job # both job_queue & conversation_timeout are checked before calling _schedule_job
j_queue = dispatcher.job_queue 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._trigger_timeout,
self.conversation_timeout, # type: ignore[arg-type] self.conversation_timeout, # type: ignore[arg-type]
context=_ConversationTimeoutContext( context=_ConversationTimeoutContext(

View file

@ -17,15 +17,15 @@
# You should have received a copy of the GNU Lesser Public License # You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/]. # along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the Dispatcher class.""" """This module contains the Dispatcher class."""
import inspect
import logging import logging
import weakref import weakref
from collections import defaultdict from collections import defaultdict
from pathlib import Path
from queue import Empty, Queue from queue import Empty, Queue
from threading import BoundedSemaphore, Event, Lock, Thread, current_thread from threading import BoundedSemaphore, Event, Lock, Thread, current_thread
from time import sleep from time import sleep
from typing import ( from typing import (
TYPE_CHECKING,
Callable, Callable,
DefaultDict, DefaultDict,
Dict, Dict,
@ -35,8 +35,7 @@ from typing import (
Union, Union,
Generic, Generic,
TypeVar, TypeVar,
overload, TYPE_CHECKING,
cast,
) )
from uuid import uuid4 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.defaultvalue import DefaultValue, DEFAULT_FALSE
from telegram.utils.warnings import warn from telegram.utils.warnings import warn
from telegram.ext.utils.promise import Promise 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: if TYPE_CHECKING:
from telegram import Bot from .jobqueue import Job
from telegram.ext import JobQueue, Job, CallbackContext from .builders import InitDispatcherBuilder
DEFAULT_GROUP: int = 0 DEFAULT_GROUP: int = 0
@ -90,24 +90,15 @@ class DispatcherHandlerStop(Exception):
self.state = state 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. """This class dispatches all kinds of updates to its registered handlers.
Args: Note:
bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers. This class may not be initialized directly. Use :class:`telegram.ext.DispatcherBuilder` or
update_queue (:obj:`Queue`): The synchronized queue that will contain the updates. :meth:`builder` (for convenience).
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.
.. versionadded:: 13.6 .. versionchanged:: 14.0
Initialization is now done through the :class:`telegram.ext.DispatcherBuilder`.
Attributes: Attributes:
bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers. 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. 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 persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
store data that should be persistent over restarts. store data that should be persistent over restarts.
context_types (:class:`telegram.ext.ContextTypes`): Container for the types used exception_event (:class:`threading.Event`): When this event is set, the dispatcher will
in the ``context`` interface. 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', 'error_handlers',
'running', 'running',
'__stop_event', '__stop_event',
'__exception_event', 'exception_event',
'__async_queue', '__async_queue',
'__async_threads', '__async_threads',
'bot', 'bot',
@ -156,51 +166,37 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
__singleton = None __singleton = None
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@overload
def __init__( def __init__(
self: 'Dispatcher[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]', self: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]',
bot: 'Bot', *,
bot: BT,
update_queue: Queue, update_queue: Queue,
workers: int = 4, job_queue: JQ,
exception_event: Event = None, workers: int,
job_queue: 'JobQueue' = None, persistence: PT,
persistence: BasePersistence = None, 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.bot = bot
self.update_queue = update_queue self.update_queue = update_queue
self.job_queue = job_queue self.job_queue = job_queue
self.workers = workers 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: if self.workers < 1:
warn( warn(
'Asynchronous callbacks can not be processed without at least one worker thread.', '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) 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 persistence:
if not isinstance(persistence, BasePersistence): if not isinstance(persistence, BasePersistence):
raise TypeError("persistence must be based on telegram.ext.BasePersistence") raise TypeError("persistence must be based on telegram.ext.BasePersistence")
self.persistence = persistence 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) self.persistence.set_bot(self.bot)
if self.persistence.store_data.user_data: if self.persistence.store_data.user_data:
self.user_data = self.persistence.get_user_data() self.user_data = self.persistence.get_user_data()
if not isinstance(self.user_data, defaultdict): 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__}" f"bot_data must be of type {self.context_types.bot_data.__name__}"
) )
if self.persistence.store_data.callback_data: if self.persistence.store_data.callback_data:
self.bot = cast(ExtBot, self.bot)
persistent_data = self.persistence.get_callback_data() persistent_data = self.persistence.get_callback_data()
if persistent_data is not None: if persistent_data is not None:
if not isinstance(persistent_data, tuple) and len(persistent_data) != 2: if not isinstance(persistent_data, tuple) and len(persistent_data) != 2:
raise ValueError('callback_data must be a 2-tuple') raise ValueError('callback_data must be a 2-tuple')
self.bot.callback_data_cache = CallbackDataCache( # Mypy doesn't know that persistence.set_bot (see above) already checks that
self.bot, # self.bot is an instance of ExtBot if callback_data should be stored ...
self.bot.callback_data_cache.maxsize, 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, persistent_data=persistent_data,
) )
else: else:
self.persistence = None self.persistence = None
self.handlers: Dict[int, List[Handler]] = {} self.handlers: Dict[int, List[Handler]] = {}
"""Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]: Holds the handlers per group."""
self.groups: List[int] = [] self.groups: List[int] = []
"""List[:obj:`int`]: A list with all groups."""
self.error_handlers: Dict[Callable, Union[bool, DefaultValue]] = {} 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 self.running = False
""":obj:`bool`: Indicates if this dispatcher is running."""
self.__stop_event = Event() self.__stop_event = Event()
self.__exception_event = exception_event or Event()
self.__async_queue: Queue = Queue() self.__async_queue: Queue = Queue()
self.__async_threads: Set[Thread] = set() self.__async_threads: Set[Thread] = set()
@ -265,9 +260,16 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
else: else:
self._set_singleton(None) self._set_singleton(None)
@property @staticmethod
def exception_event(self) -> Event: # skipcq: PY-D0003 def builder() -> 'InitDispatcherBuilder':
return self.__exception_event """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: def _init_async_threads(self, base_name: str, workers: int) -> None:
base_name = f'{base_name}_' if base_name else '' 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: def start(self, ready: Event = None) -> None:
"""Thread target of thread 'dispatcher'. """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: Args:
ready (:obj:`threading.Event`, optional): If specified, the event will be set once the 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() ready.set()
return return
if self.__exception_event.is_set(): if self.exception_event.is_set():
msg = 'reusing dispatcher after exception event is forbidden' msg = 'reusing dispatcher after exception event is forbidden'
self.logger.error(msg) self.logger.error(msg)
raise TelegramError(msg) raise TelegramError(msg)
if self.job_queue:
self.job_queue.start()
self._init_async_threads(str(uuid4()), self.workers) self._init_async_threads(str(uuid4()), self.workers)
self.running = True self.running = True
self.logger.debug('Dispatcher started') self.logger.debug('Dispatcher started')
@ -401,7 +405,7 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
if self.__stop_event.is_set(): if self.__stop_event.is_set():
self.logger.debug('orderly stopping') self.logger.debug('orderly stopping')
break break
if self.__exception_event.is_set(): if self.exception_event.is_set():
self.logger.critical('stopping due to exception in another thread') self.logger.critical('stopping due to exception in another thread')
break break
continue continue
@ -414,7 +418,10 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
self.logger.debug('Dispatcher thread stopped') self.logger.debug('Dispatcher thread stopped')
def stop(self) -> None: 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: if self.running:
self.__stop_event.set() self.__stop_event.set()
while self.running: while self.running:
@ -436,6 +443,17 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
self.__async_threads.remove(thr) self.__async_threads.remove(thr)
self.logger.debug('async thread %s/%s has ended', i + 1, total) 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 @property
def has_running_threads(self) -> bool: # skipcq: PY-D0003 def has_running_threads(self) -> bool: # skipcq: PY-D0003
return self.running or bool(self.__async_threads) return self.running or bool(self.__async_threads)
@ -602,10 +620,11 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
user_ids = [] user_ids = []
if self.persistence.store_data.callback_data: if self.persistence.store_data.callback_data:
self.bot = cast(ExtBot, self.bot)
try: 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.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: except Exception as exc:
self.dispatch_error(update, exc) self.dispatch_error(update, exc)
@ -641,11 +660,8 @@ class Dispatcher(Generic[CCT, UD, CD, BD]):
Args: Args:
callback (:obj:`callable`): The callback function for this error handler. Will be callback (:obj:`callable`): The callback function for this error handler. Will be
called when an error is raised. called when an error is raised. Callback signature:
Callback signature: ``def callback(update: Update, context: CallbackContext)``
``def callback(update: Update, context: CallbackContext)``
The error that happened will be present in context.error. The error that happened will be present in context.error.
run_async (:obj:`bool`, optional): Whether this handlers callback should be run 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__( def __init__(
self, self,
token: str, token: str,
base_url: str = None, base_url: str = 'https://api.telegram.org/bot',
base_file_url: str = None, base_file_url: str = 'https://api.telegram.org/file/bot',
request: 'Request' = None, request: 'Request' = None,
private_key: bytes = None, private_key: bytes = None,
private_key_password: bytes = None, private_key_password: bytes = None,

View file

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

View file

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

View file

@ -17,42 +17,42 @@
# You should have received a copy of the GNU Lesser Public License # You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/]. # 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.""" """This module contains the class Updater, which tries to make creating Telegram bots intuitive."""
import inspect
import logging import logging
import ssl import ssl
import signal import signal
from pathlib import Path
from queue import Queue from queue import Queue
from threading import Event, Lock, Thread, current_thread from threading import Event, Lock, Thread, current_thread
from time import sleep from time import sleep
from typing import ( from typing import (
TYPE_CHECKING,
Any, Any,
Callable, Callable,
Dict,
List, List,
Optional, Optional,
Tuple, Tuple,
Union, Union,
no_type_check, no_type_check,
Generic, Generic,
overload, TypeVar,
TYPE_CHECKING,
) )
from telegram import Bot
from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized, TelegramError from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized, TelegramError
from telegram.ext import Dispatcher, JobQueue, ContextTypes, ExtBot from telegram.ext import Dispatcher
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.utils.webhookhandler import WebhookAppClass, WebhookServer 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: 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 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 :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. WebhookHandler classes.
Note: Note:
* You must supply either a :attr:`bot` or a :attr:`token` argument. This class may not be initialized directly. Use :class:`telegram.ext.UpdaterBuilder` or
* If you supply a :attr:`bot`, you will need to pass :attr:`arbitrary_callback_data`, :meth:`builder` (for convenience).
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.
.. 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: Attributes:
bot (:class:`telegram.Bot`): The bot used with this Updater. 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. received.
.. versionchanged:: 14.0
Renamed ``user_sig_handler`` to ``user_signal_handler``.
update_queue (:obj:`Queue`): Queue for the updates. update_queue (:obj:`Queue`): Queue for the updates.
job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater. dispatcher (:class:`telegram.ext.Dispatcher`): Optional. Dispatcher that handles the
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and updates and dispatches them to the handlers.
dispatches them to the handlers.
running (:obj:`bool`): Indicates if the updater is running. running (:obj:`bool`): Indicates if the updater is running.
persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to exception_event (:class:`threading.Event`): When an unhandled exception happens while
store data that should be persistent over restarts. 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__ = ( __slots__ = (
'persistence',
'dispatcher', 'dispatcher',
'user_sig_handler', 'user_signal_handler',
'bot', 'bot',
'logger', 'logger',
'update_queue', 'update_queue',
'job_queue', 'exception_event',
'__exception_event',
'last_update_id', 'last_update_id',
'running', 'running',
'_request',
'is_idle', 'is_idle',
'httpd', 'httpd',
'__lock', '__lock',
'__threads', '__threads',
) )
@overload
def __init__( def __init__(
self: 'Updater[CallbackContext, dict, dict, dict]', self: 'Updater[BT, DT]',
token: str = None, *,
base_url: str = None, user_signal_handler: Callable[[int, object], Any] = None,
workers: int = 4, dispatcher: DT = None,
bot: Bot = None, bot: BT = None,
private_key: bytes = None, update_queue: Queue = None,
private_key_password: bytes = None, exception_event: Event = 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,
): ):
... if not was_called_by(
inspect.currentframe(), Path(__file__).parent.resolve() / 'builders.py'
@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:
warn( warn(
'Passing defaults to an Updater has no effect when a Bot is passed ' '`Updater` instances should be built via the `UpdaterBuilder`.',
'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.',
stacklevel=2, stacklevel=2,
) )
if dispatcher is None: self.user_signal_handler = user_signal_handler
if (token is None) and (bot is None): self.dispatcher = dispatcher
raise ValueError('`token` or `bot` must be passed') if self.dispatcher:
if (token is not None) and (bot is not None): self.bot = self.dispatcher.bot
raise ValueError('`token` and `bot` are mutually exclusive') self.update_queue = self.dispatcher.update_queue
if (private_key is not None) and (bot is not None): self.exception_event = self.dispatcher.exception_event
raise ValueError('`bot` and `private_key` are mutually exclusive')
else: else:
if bot is not None: self.bot = bot
raise ValueError('`dispatcher` and `bot` are mutually exclusive') self.update_queue = update_queue
if persistence is not None: self.exception_event = exception_event
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.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.last_update_id = 0
self.running = False self.running = False
self.is_idle = False self.is_idle = False
self.httpd = None self.httpd = None
self.__lock = Lock() self.__lock = Lock()
self.__threads: List[Thread] = [] 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: def _init_thread(self, target: Callable, name: str, *args: object, **kwargs: object) -> None:
thr = Thread( thr = Thread(
@ -335,7 +170,7 @@ class Updater(Generic[CCT, UD, CD, BD]):
try: try:
target(*args, **kwargs) target(*args, **kwargs)
except Exception: except Exception:
self.__exception_event.set() self.exception_event.set()
self.logger.exception('unhandled exception in %s', thr_name) self.logger.exception('unhandled exception in %s', thr_name)
raise raise
self.logger.debug('%s - ended', thr_name) self.logger.debug('%s - ended', thr_name)
@ -384,10 +219,11 @@ class Updater(Generic[CCT, UD, CD, BD]):
self.running = True self.running = True
# Create & start threads # Create & start threads
self.job_queue.start()
dispatcher_ready = Event() dispatcher_ready = Event()
polling_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._init_thread(
self._start_polling, self._start_polling,
"updater", "updater",
@ -400,9 +236,11 @@ class Updater(Generic[CCT, UD, CD, BD]):
ready=polling_ready, ready=polling_ready,
) )
self.logger.debug('Waiting for Dispatcher and polling to start') self.logger.debug('Waiting for polling to start')
dispatcher_ready.wait()
polling_ready.wait() 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 the update queue so the main thread can insert updates
return self.update_queue return self.update_queue
@ -478,8 +316,9 @@ class Updater(Generic[CCT, UD, CD, BD]):
# Create & start threads # Create & start threads
webhook_ready = Event() webhook_ready = Event()
dispatcher_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._init_thread(
self._start_webhook, self._start_webhook,
"updater", "updater",
@ -497,9 +336,11 @@ class Updater(Generic[CCT, UD, CD, BD]):
max_connections=max_connections, 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() 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 the update queue so the main thread can insert updates
return self.update_queue 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) webhook_url = self._gen_webhook_url(listen, port, url_path)
# We pass along the cert to the webhook if present. # We pass along the cert to the webhook if present.
cert_file = open(cert, 'rb') if cert is not None else None if cert is not None:
self._bootstrap( with open(cert, 'rb') as cert_file:
max_retries=bootstrap_retries, self._bootstrap(
drop_pending_updates=drop_pending_updates, cert=cert_file,
webhook_url=webhook_url, max_retries=bootstrap_retries,
allowed_updates=allowed_updates, drop_pending_updates=drop_pending_updates,
cert=cert_file, webhook_url=webhook_url,
ip_address=ip_address, allowed_updates=allowed_updates,
max_connections=max_connections, ip_address=ip_address,
) max_connections=max_connections,
if cert_file is not None: )
cert_file.close() 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) self.httpd.serve_forever(ready=ready)
@ -750,10 +599,11 @@ class Updater(Generic[CCT, UD, CD, BD]):
def stop(self) -> None: def stop(self) -> None:
"""Stops the polling/webhook thread, the dispatcher and the job queue.""" """Stops the polling/webhook thread, the dispatcher and the job queue."""
self.job_queue.stop()
with self.__lock: with self.__lock:
if self.running or self.dispatcher.has_running_threads: if self.running or (self.dispatcher and self.dispatcher.has_running_threads):
self.logger.debug('Stopping Updater and Dispatcher...') self.logger.debug(
'Stopping Updater %s...', 'and Dispatcher ' if self.dispatcher else ''
)
self.running = False self.running = False
@ -761,9 +611,10 @@ class Updater(Generic[CCT, UD, CD, BD]):
self._stop_dispatcher() self._stop_dispatcher()
self._join_threads() self._join_threads()
# Stop the Request instance only if it was created by the Updater # Clear the connection pool only if the bot is managed by the Updater
if self._request: # Otherwise `dispatcher.stop()` already does that
self._request.stop() if not self.dispatcher:
self.bot.request.stop()
@no_type_check @no_type_check
def _stop_httpd(self) -> None: def _stop_httpd(self) -> None:
@ -778,8 +629,9 @@ class Updater(Generic[CCT, UD, CD, BD]):
@no_type_check @no_type_check
def _stop_dispatcher(self) -> None: def _stop_dispatcher(self) -> None:
self.logger.debug('Requesting Dispatcher to stop...') if self.dispatcher:
self.dispatcher.stop() self.logger.debug('Requesting Dispatcher to stop...')
self.dispatcher.stop()
@no_type_check @no_type_check
def _join_threads(self) -> None: def _join_threads(self) -> None:
@ -801,13 +653,9 @@ class Updater(Generic[CCT, UD, CD, BD]):
# https://bugs.python.org/issue28206 # https://bugs.python.org/issue28206
signal.Signals(signum), # pylint: disable=no-member 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() self.stop()
if self.user_sig_handler: if self.user_signal_handler:
self.user_sig_handler(signum, frame) self.user_signal_handler(signum, frame)
else: else:
self.logger.warning('Exiting immediately!') self.logger.warning('Exiting immediately!')
# pylint: disable=import-outside-toplevel, protected-access # 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 user. Changes to this module are not considered breaking changes and may not be documented in
the changelog. 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: 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]] ConversationDict = Dict[Tuple[int, ...], Optional[object]]
@ -50,6 +51,11 @@ CCT = TypeVar('CCT', bound='CallbackContext')
.. versionadded:: 13.6 .. versionadded:: 13.6
""" """
BT = TypeVar('BT', bound='Bot')
"""Type of the bot.
.. versionadded:: 14.0
"""
UD = TypeVar('UD') UD = TypeVar('UD')
"""Type of the user data for a single user. """Type of the user data for a single user.
@ -65,3 +71,11 @@ BD = TypeVar('BD')
.. versionadded:: 13.6 .. 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 import certifi
try: try:
import telegram.vendor.ptb_urllib3.urllib3 as urllib3 from telegram.vendor.ptb_urllib3 import urllib3
import telegram.vendor.ptb_urllib3.urllib3.contrib.appengine as appengine from telegram.vendor.ptb_urllib3.urllib3.contrib import appengine
from telegram.vendor.ptb_urllib3.urllib3.connection import HTTPConnection from telegram.vendor.ptb_urllib3.urllib3.connection import HTTPConnection
from telegram.vendor.ptb_urllib3.urllib3.fields import RequestField from telegram.vendor.ptb_urllib3.urllib3.fields import RequestField
from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
try: try:
import urllib3 # type: ignore[no-redef] 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.connection import HTTPConnection # type: ignore[no-redef]
from urllib3.fields import RequestField # type: ignore[no-redef] from urllib3.fields import RequestField # type: ignore[no-redef]
from urllib3.util.timeout import Timeout # 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 ( from telegram.ext import (
Dispatcher, Dispatcher,
JobQueue,
Updater,
MessageFilter, MessageFilter,
Defaults, Defaults,
UpdateFilter, UpdateFilter,
ExtBot, ExtBot,
DispatcherBuilder,
UpdaterBuilder,
) )
from telegram.error import BadRequest from telegram.error import BadRequest
from telegram.utils.defaultvalue import DefaultValue, DEFAULT_NONE from telegram.utils.defaultvalue import DefaultValue, DEFAULT_NONE
@ -173,8 +173,7 @@ def provider_token(bot_info):
def create_dp(bot): def create_dp(bot):
# Dispatcher is heavy to init (due to many threads and such) so we have a single session # 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) # scoped one here, but before each test, reset it (dp fixture below)
dispatcher = DictDispatcher(bot, Queue(), job_queue=JobQueue(), workers=2) dispatcher = DispatcherBuilder().bot(bot).workers(2).dispatcher_class(DictDispatcher).build()
dispatcher.job_queue.set_dispatcher(dispatcher)
thr = Thread(target=dispatcher.start) thr = Thread(target=dispatcher.start)
thr.start() thr.start()
sleep(2) sleep(2)
@ -202,7 +201,7 @@ def dp(_dp):
_dp.handlers = {} _dp.handlers = {}
_dp.groups = [] _dp.groups = []
_dp.error_handlers = {} _dp.error_handlers = {}
_dp.__exception_event = Event() _dp.exception_event = Event()
_dp.__stop_event = Event() _dp.__stop_event = Event()
_dp.__async_queue = Queue() _dp.__async_queue = Queue()
_dp.__async_threads = set() _dp.__async_threads = set()
@ -212,7 +211,7 @@ def dp(_dp):
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def updater(bot): def updater(bot):
up = Updater(bot=bot, workers=2) up = UpdaterBuilder().bot(bot).workers(2).build()
yield up yield up
if up.running: if up.running:
up.stop() up.stop()

View file

@ -184,7 +184,7 @@ class TestBot:
@flaky(3, 1) @flaky(3, 1)
def test_invalid_token_server_response(self, monkeypatch): 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') bot = Bot('12')
with pytest.raises(InvalidToken): with pytest.raises(InvalidToken):
bot.get_me() 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, JobQueue,
BasePersistence, BasePersistence,
ContextTypes, ContextTypes,
DispatcherBuilder,
UpdaterBuilder,
) )
from telegram.ext import PersistenceInput from telegram.ext import PersistenceInput
from telegram.ext.dispatcher import Dispatcher, DispatcherHandlerStop from telegram.ext.dispatcher import Dispatcher, DispatcherHandlerStop
@ -98,14 +100,36 @@ class TestDispatcher:
self.received = context.error.message self.received = context.error.message
def test_slot_behaviour(self, bot, mro_slots): 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__: for at in dp.__slots__:
at = f"_Dispatcher{at}" if at.startswith('__') and not at.endswith('__') else at 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 getattr(dp, at, 'err') != 'err', f"got extra slot '{at}'"
assert len(mro_slots(dp)) == len(set(mro_slots(dp))), "duplicate slot" assert len(mro_slots(dp)) == len(set(mro_slots(dp))), "duplicate slot"
def test_less_than_one_worker_warning(self, dp, recwarn): def test_manual_init_warning(self, recwarn):
Dispatcher(dp.bot, dp.update_queue, job_queue=dp.job_queue, workers=0) 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 len(recwarn) == 1
assert ( assert (
str(recwarn[0].message) str(recwarn[0].message)
@ -113,6 +137,18 @@ class TestDispatcher:
) )
assert recwarn[0].filename == __file__, "stacklevel is incorrect!" 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 test_one_context_per_update(self, dp):
def one(update, context): def one(update, context):
if update.message.text == 'test': if update.message.text == 'test':
@ -163,7 +199,7 @@ class TestDispatcher:
with pytest.raises( with pytest.raises(
TypeError, match='persistence must be based on telegram.ext.BasePersistence' 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): def test_error_handler_that_raises_errors(self, dp):
""" """
@ -580,7 +616,7 @@ class TestDispatcher:
), ),
) )
my_persistence = OwnPersistence() 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_handler(CommandHandler('start', start1))
dp.add_error_handler(error) dp.add_error_handler(error)
dp.process_update(update) dp.process_update(update)
@ -885,7 +921,7 @@ class TestDispatcher:
bot_data=complex, 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.user_data[1], int)
assert isinstance(dispatcher.chat_data[1], float) assert isinstance(dispatcher.chat_data[1], float)
@ -900,12 +936,15 @@ class TestDispatcher:
type(context.bot_data), type(context.bot_data),
) )
dispatcher = Dispatcher( dispatcher = (
bot, DispatcherBuilder()
Queue(), .bot(bot)
context_types=ContextTypes( .context_types(
context=CustomContext, bot_data=int, user_data=float, chat_data=complex ContextTypes(
), context=CustomContext, bot_data=int, user_data=float, chat_data=complex
)
)
.build()
) )
dispatcher.add_error_handler(error_handler) dispatcher.add_error_handler(error_handler)
dispatcher.add_handler(MessageHandler(Filters.all, self.callback_raise_error)) dispatcher.add_handler(MessageHandler(Filters.all, self.callback_raise_error))
@ -923,12 +962,15 @@ class TestDispatcher:
type(context.bot_data), type(context.bot_data),
) )
dispatcher = Dispatcher( dispatcher = (
bot, DispatcherBuilder()
Queue(), .bot(bot)
context_types=ContextTypes( .context_types(
context=CustomContext, bot_data=int, user_data=float, chat_data=complex ContextTypes(
), context=CustomContext, bot_data=int, user_data=float, chat_data=complex
)
)
.build()
) )
dispatcher.add_handler(MessageHandler(Filters.all, callback)) dispatcher.add_handler(MessageHandler(Filters.all, callback))

View file

@ -29,7 +29,13 @@ import pytest
import pytz import pytz
from apscheduler.schedulers import SchedulerNotRunningError from apscheduler.schedulers import SchedulerNotRunningError
from flaky import flaky 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): class CustomContext(CallbackContext):
@ -55,11 +61,6 @@ class TestJobQueue:
job_time = 0 job_time = 0
received_error = None 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) @pytest.fixture(autouse=True)
def reset(self): def reset(self):
self.result = 0 self.result = 0
@ -100,6 +101,22 @@ class TestJobQueue:
def error_handler_raise_error(self, *args): def error_handler_raise_error(self, *args):
raise Exception('Failing bigly') 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): def test_run_once(self, job_queue):
job_queue.run_once(self.job_run_once, 0.01) job_queue.run_once(self.job_run_once, 0.01)
sleep(0.02) sleep(0.02)
@ -228,19 +245,19 @@ class TestJobQueue:
sleep(0.03) sleep(0.03)
assert self.result == 1 assert self.result == 1
def test_in_updater(self, bot): def test_in_dispatcher(self, bot):
u = Updater(bot=bot) dispatcher = DispatcherBuilder().bot(bot).build()
u.job_queue.start() dispatcher.job_queue.start()
try: 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) sleep(0.03)
assert self.result == 1 assert self.result == 1
u.stop() dispatcher.stop()
sleep(1) sleep(1)
assert self.result == 1 assert self.result == 1
finally: finally:
try: try:
u.stop() dispatcher.stop()
except SchedulerNotRunningError: except SchedulerNotRunningError:
pass pass
@ -479,12 +496,15 @@ class TestJobQueue:
assert 'No error handlers are registered' in rec.getMessage() assert 'No error handlers are registered' in rec.getMessage()
def test_custom_context(self, bot, job_queue): def test_custom_context(self, bot, job_queue):
dispatcher = Dispatcher( dispatcher = (
bot, DispatcherBuilder()
Queue(), .bot(bot)
context_types=ContextTypes( .context_types(
context=CustomContext, bot_data=int, user_data=float, chat_data=complex ContextTypes(
), context=CustomContext, bot_data=int, user_data=float, chat_data=complex
)
)
.build()
) )
job_queue.set_dispatcher(dispatcher) job_queue.set_dispatcher(dispatcher)

View file

@ -22,7 +22,7 @@ import uuid
from pathlib import Path from pathlib import Path
from threading import Lock from threading import Lock
from telegram.ext import PersistenceInput from telegram.ext import PersistenceInput, UpdaterBuilder
from telegram.ext.callbackdatacache import CallbackDataCache from telegram.ext.callbackdatacache import CallbackDataCache
try: try:
@ -41,7 +41,6 @@ import pytest
from telegram import Update, Message, User, Chat, MessageEntity, Bot from telegram import Update, Message, User, Chat, MessageEntity, Bot
from telegram.ext import ( from telegram.ext import (
BasePersistence, BasePersistence,
Updater,
ConversationHandler, ConversationHandler,
MessageHandler, MessageHandler,
Filters, Filters,
@ -215,7 +214,7 @@ def conversations():
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def updater(bot, base_persistence): def updater(bot, base_persistence):
base_persistence.store_data = PersistenceInput(False, False, False, False) 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() base_persistence.store_data = PersistenceInput()
return u return u
@ -304,34 +303,36 @@ class TestBasePersistence:
base_persistence.get_callback_data = get_callback_data base_persistence.get_callback_data = get_callback_data
with pytest.raises(ValueError, match="user_data must be of type defaultdict"): 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(): def get_user_data():
return user_data return user_data
base_persistence.get_user_data = get_user_data base_persistence.get_user_data = get_user_data
with pytest.raises(ValueError, match="chat_data must be of type defaultdict"): 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(): def get_chat_data():
return chat_data return chat_data
base_persistence.get_chat_data = get_chat_data base_persistence.get_chat_data = get_chat_data
with pytest.raises(ValueError, match="bot_data must be of type dict"): 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(): def get_bot_data():
return bot_data return bot_data
base_persistence.get_bot_data = get_bot_data base_persistence.get_bot_data = get_bot_data
with pytest.raises(ValueError, match="callback_data must be a 2-tuple"): 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(): def get_callback_data():
return callback_data return callback_data
base_persistence.bot = None
base_persistence.get_callback_data = get_callback_data 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.bot_data == bot_data
assert u.dispatcher.chat_data == chat_data assert u.dispatcher.chat_data == chat_data
assert u.dispatcher.user_data == user_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_bot_data = lambda x: x
base_persistence.refresh_chat_data = lambda x, y: x base_persistence.refresh_chat_data = lambda x, y: x
base_persistence.refresh_user_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 dp = updater.dispatcher
def callback_known_user(update, context): def callback_known_user(update, context):
@ -1622,7 +1623,7 @@ class TestPicklePersistence:
assert conversations_test['name1'] == conversation1 assert conversations_test['name1'] == conversation1
def test_with_handler(self, bot, update, bot_data, pickle_persistence, good_pickle_files): 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 dp = u.dispatcher
bot.callback_data_cache.clear_callback_data() bot.callback_data_cache.clear_callback_data()
bot.callback_data_cache.clear_callback_queries() bot.callback_data_cache.clear_callback_queries()
@ -1660,13 +1661,13 @@ class TestPicklePersistence:
single_file=False, single_file=False,
on_flush=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 = u.dispatcher
dp.add_handler(h2) dp.add_handler(h2)
dp.process_update(update) dp.process_update(update)
def test_flush_on_stop(self, bot, update, pickle_persistence): 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 dp = u.dispatcher
u.running = True u.running = True
dp.user_data[4242424242]['my_test'] = 'Working!' dp.user_data[4242424242]['my_test'] = 'Working!'
@ -1686,7 +1687,7 @@ class TestPicklePersistence:
assert data['test'] == 'Working4!' assert data['test'] == 'Working4!'
def test_flush_on_stop_only_bot(self, bot, update, pickle_persistence_only_bot): 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 dp = u.dispatcher
u.running = True u.running = True
dp.user_data[4242424242]['my_test'] = 'Working!' dp.user_data[4242424242]['my_test'] = 'Working!'
@ -1706,7 +1707,7 @@ class TestPicklePersistence:
assert pickle_persistence_2.get_callback_data() is None assert pickle_persistence_2.get_callback_data() is None
def test_flush_on_stop_only_chat(self, bot, update, pickle_persistence_only_chat): 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 dp = u.dispatcher
u.running = True u.running = True
dp.user_data[4242424242]['my_test'] = 'Working!' dp.user_data[4242424242]['my_test'] = 'Working!'
@ -1726,7 +1727,7 @@ class TestPicklePersistence:
assert pickle_persistence_2.get_callback_data() is None assert pickle_persistence_2.get_callback_data() is None
def test_flush_on_stop_only_user(self, bot, update, pickle_persistence_only_user): 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 dp = u.dispatcher
u.running = True u.running = True
dp.user_data[4242424242]['my_test'] = 'Working!' dp.user_data[4242424242]['my_test'] = 'Working!'
@ -1746,7 +1747,7 @@ class TestPicklePersistence:
assert pickle_persistence_2.get_callback_data() is None assert pickle_persistence_2.get_callback_data() is None
def test_flush_on_stop_only_callback(self, bot, update, pickle_persistence_only_callback): 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 dp = u.dispatcher
u.running = True u.running = True
dp.user_data[4242424242]['my_test'] = 'Working!' dp.user_data[4242424242]['my_test'] = 'Working!'
@ -2194,7 +2195,7 @@ class TestDictPersistence:
def test_with_handler(self, bot, update): def test_with_handler(self, bot, update):
dict_persistence = DictPersistence() dict_persistence = DictPersistence()
u = Updater(bot=bot, persistence=dict_persistence) u = UpdaterBuilder().bot(bot).persistence(dict_persistence).build()
dp = u.dispatcher dp = u.dispatcher
def first(update, context): def first(update, context):
@ -2236,7 +2237,7 @@ class TestDictPersistence:
callback_data_json=callback_data, 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 = u.dispatcher
dp.add_handler(h2) dp.add_handler(h2)
dp.process_update(update) 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.error import Unauthorized, InvalidToken, TimedOut, RetryAfter, TelegramError
from telegram.ext import ( from telegram.ext import (
Updater,
Dispatcher,
DictPersistence,
Defaults,
InvalidCallbackData, InvalidCallbackData,
ExtBot, ExtBot,
Updater,
UpdaterBuilder,
DispatcherBuilder,
) )
from telegram.warnings import PTBDeprecationWarning
from telegram.ext.utils.webhookhandler import WebhookServer from telegram.ext.utils.webhookhandler import WebhookServer
signalskip = pytest.mark.skipif( signalskip = pytest.mark.skipif(
@ -90,12 +88,6 @@ class TestUpdater:
offset = 0 offset = 0
test_flag = False 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) @pytest.fixture(autouse=True)
def reset(self): def reset(self):
self.message_count = 0 self.message_count = 0
@ -113,18 +105,49 @@ class TestUpdater:
self.received = update.message.text self.received = update.message.text
self.cb_handler_called.set() self.cb_handler_called.set()
def test_warn_arbitrary_callback_data(self, bot, recwarn): def test_slot_behaviour(self, updater, mro_slots):
Updater(bot=bot, arbitrary_callback_data=True) 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 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): def test_warn_con_pool(self, bot, recwarn, dp):
dp = Dispatcher(bot, Queue(), workers=5) DispatcherBuilder().bot(bot).workers(5).build()
Updater(bot=bot, workers=8) UpdaterBuilder().bot(bot).workers(8).build()
Updater(dispatcher=dp, workers=None) UpdaterBuilder().bot(bot).workers(2).build()
assert len(recwarn) == 2 assert len(recwarn) == 2
for idx, value in enumerate((12, 9)): for idx, value in enumerate((9, 12)):
warning = f'Connection pool of Request object is smaller than optimal value {value}' warning = (
'The Connection pool of Request object is smaller (8) than the '
f'recommended value of {value}.'
)
assert str(recwarn[idx].message) == warning assert str(recwarn[idx].message) == warning
assert recwarn[idx].filename == __file__, "wrong stacklevel!" 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_data()
updater.bot.callback_data_cache.clear_callback_queries() 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, 'set_webhook', lambda *args, **kwargs: True)
monkeypatch.setattr(updater.bot, 'delete_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 # 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)) 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.start_webhook(ip, port)
updater.stop() updater.stop()
assert not caplog.records 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): def test_webhook_ssl(self, monkeypatch, updater):
monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True)
@ -606,7 +643,7 @@ class TestUpdater:
def user_signal_inc(signum, frame): def user_signal_inc(signum, frame):
temp_var['a'] = 1 temp_var['a'] = 1
updater.user_sig_handler = user_signal_inc updater.user_signal_handler = user_signal_inc
updater.start_polling(0.01) updater.start_polling(0.01)
Thread(target=partial(self.signal_sender, updater=updater)).start() Thread(target=partial(self.signal_sender, updater=updater)).start()
updater.idle() updater.idle()
@ -614,47 +651,3 @@ class TestUpdater:
sleep(0.5) sleep(0.5)
assert updater.running is False assert updater.running is False
assert temp_var['a'] != 0 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())