mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2026-06-27 19:54:38 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98147fce32 | |||
| e54e9f2347 | |||
| 3545139dd7 | |||
| d0c27e2d46 | |||
| 3318239cf6 | |||
| aadb6df271 | |||
| 2cc9aac7dc | |||
| 1d007b1b60 | |||
| 3257148d13 | |||
| 805a798b50 |
@@ -10,9 +10,6 @@ repos:
|
||||
sha: 0b70e285e369bcb24b57b74929490ea7be9c4b19
|
||||
hooks:
|
||||
- id: flake8
|
||||
exclude: ^(setup.py|docs/source/conf.py)$
|
||||
args:
|
||||
- --ignore=W605,W503
|
||||
- repo: git://github.com/pre-commit/mirrors-pylint
|
||||
sha: 9d8dcbc2b86c796275680f239c1e90dcd50bd398
|
||||
hooks:
|
||||
|
||||
+8
-6
@@ -14,10 +14,11 @@ matrix:
|
||||
dist: xenial
|
||||
- python: pypy3.5-5.10.1
|
||||
dist: xenial
|
||||
- python: 3.8-dev
|
||||
dist: xenial
|
||||
allow_failures:
|
||||
- python: pypy2.7-5.10.0
|
||||
- python: pypy3.5-5.10.1
|
||||
- env: TEST_OFFICIAL=true
|
||||
|
||||
dist: trusty
|
||||
sudo: false
|
||||
@@ -36,17 +37,18 @@ before_cache:
|
||||
- rm -f $HOME/.pre-commit/pre-commit.log
|
||||
|
||||
install:
|
||||
# fix TypeError from old version of this
|
||||
- pip install -U codecov pytest-cov
|
||||
- echo $TRAVIS_PYTHON_VERSION
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '3.7'* ]]; then pip install -U git+https://github.com/yaml/pyyaml.git; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '3.7'* ]]; then pip install -U git+https://github.com/yaml/pyyaml.git; else true; fi
|
||||
- pip install -U -r requirements.txt
|
||||
- pip install -U -r requirements-dev.txt
|
||||
- if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then pip install ujson; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then pip install ujson; else true; fi
|
||||
|
||||
script:
|
||||
- if [[ $TEST_OFFICIAL != 'true' ]]; then pytest -v -m nocoverage; fi
|
||||
- if [[ $TEST_OFFICIAL != 'true' ]]; then pytest -v -m "not nocoverage" --cov; fi
|
||||
- if [[ $TEST_OFFICIAL == 'true' ]]; then pytest -v tests/test_official.py; fi
|
||||
- if [[ $TEST_OFFICIAL != 'true' ]]; then pytest -v -m nocoverage; else true; fi
|
||||
- if [[ $TEST_OFFICIAL != 'true' ]]; then pytest -v -m "not nocoverage" --cov; else true; fi
|
||||
- if [[ $TEST_OFFICIAL == 'true' ]]; then pytest -v tests/test_official.py; else true; fi
|
||||
|
||||
after_success:
|
||||
- coverage combine
|
||||
|
||||
@@ -72,6 +72,7 @@ The following wonderful people contributed directly or indirectly to this projec
|
||||
- `Shelomentsev D <https://github.com/shelomentsevd>`_
|
||||
- `Simon Schürrle <https://github.com/SitiSchu>`_
|
||||
- `sooyhwang <https://github.com/sooyhwang>`_
|
||||
- `syntx <https://github.com/syntx>`_
|
||||
- `thodnev <https://github.com/thodnev>`_
|
||||
- `Trainer Jono <https://github.com/Tr-Jono>`_
|
||||
- `Valentijn <https://github.com/Faalentijn>`_
|
||||
|
||||
+27
@@ -2,6 +2,33 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Version 12.2.0
|
||||
==============
|
||||
|
||||
**New features:**
|
||||
|
||||
- Nested ConversationHandlers (`#1512`_).
|
||||
|
||||
**Minor changes, CI improvments or bug fixes:**
|
||||
|
||||
- Fix CI failures due to non-backward compat attrs depndency (`#1540`_).
|
||||
- travis.yaml: TEST_OFFICIAL removed from allowed_failures.
|
||||
- Fix typos in examples (`#1537`_).
|
||||
- Fix Bot.to_dict to use proper first_name (`#1525`_).
|
||||
- Refactor ``test_commandhandler.py`` (`#1408`_).
|
||||
- Add Python 3.8 (RC version) to Travis testing matrix (`#1543`_).
|
||||
- test_bot.py: Add to_dict test (`#1544`_).
|
||||
- Flake config moved into setup.cfg (`#1546`_).
|
||||
|
||||
.. _`#1512`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1512
|
||||
.. _`#1540`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1540
|
||||
.. _`#1537`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1537
|
||||
.. _`#1525`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1525
|
||||
.. _`#1408`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1408
|
||||
.. _`#1543`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1543
|
||||
.. _`#1544`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1544
|
||||
.. _`#1546`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1546
|
||||
|
||||
Version 12.1.1
|
||||
==============
|
||||
*Released 2019-09-18*
|
||||
|
||||
+5
-1
@@ -2,7 +2,7 @@ environment:
|
||||
|
||||
matrix:
|
||||
# For Python versions available on Appveyor, see
|
||||
# http://www.appveyor.com/docs/installed-software#python
|
||||
# https://www.appveyor.com/docs/windows-images-software/#python
|
||||
# The list here is complete (excluding Python 2.6, which
|
||||
# isn't covered by this document) at the time of writing.
|
||||
|
||||
@@ -10,6 +10,7 @@ environment:
|
||||
- PYTHON: "C:\\Python35"
|
||||
- PYTHON: "C:\\Python36"
|
||||
- PYTHON: "C:\\Python37"
|
||||
# - PYTHON: "C:\\Python38"
|
||||
|
||||
branches:
|
||||
only:
|
||||
@@ -26,6 +27,8 @@ install:
|
||||
# Check that we have the expected version and architecture for Python
|
||||
- "python --version"
|
||||
# We need wheel installed to build wheels
|
||||
# fix TypeError from an old version of this
|
||||
- "pip install attrs==17.4.0"
|
||||
- "pip install -U codecov pytest-cov"
|
||||
- "pip install -r requirements.txt"
|
||||
- "pip install -r requirements-dev.txt"
|
||||
@@ -33,6 +36,7 @@ install:
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- "pytest --version"
|
||||
- "pytest -m \"not nocoverage\" --cov --cov-report xml:coverage.xml"
|
||||
|
||||
after_test:
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ author = u'Leandro Toledo'
|
||||
# The short X.Y version.
|
||||
version = '12.1' # telegram.__version__[:3]
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '12.1.1' # telegram.__version__
|
||||
release = '12.2.0' # telegram.__version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
||||
@@ -16,6 +16,9 @@ A common task for a bot is to ask information from the user. In v5.0 of this lib
|
||||
### [`conversationbot2.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/conversationbot2.py)
|
||||
A more complex example of a bot that uses the `ConversationHandler`. It is also more confusing. Good thing there is a [fancy state diagram](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/conversationbot2.png) for this one, too!
|
||||
|
||||
### [`nestedconversationbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/nestedconversationbot.py)
|
||||
A even more complex example of a bot that uses the nested `ConversationHandler`s. While it's certainly not that complex that you couldn't built it without nested `ConversationHanldler`s, it gives a good impression on how to work with them. Of course, there is a [fancy state diagram](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/nestedconversationbot.png) for this example, too!
|
||||
|
||||
### [`inlinekeyboard.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/inlinekeyboard.py)
|
||||
This example sheds some light on inline keyboards, callback queries and message editing.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Define constants the will allow us to reuse the deep-linking parameters.
|
||||
# Define constants that will allow us to reuse the deep-linking parameters.
|
||||
CHECK_THIS_OUT = 'check-this-out'
|
||||
USING_ENTITIES = 'using-entities-here'
|
||||
SO_COOL = 'so-cool'
|
||||
|
||||
@@ -26,8 +26,8 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Define a few command handlers. These usually take the two arguments bot and
|
||||
# update. Error handlers also receive the raised TelegramError object in error.
|
||||
# Define a few command handlers. These usually take the two arguments update and
|
||||
# context. Error handlers also receive the raised TelegramError object in error.
|
||||
def start(update, context):
|
||||
"""Send a message when the command /start is issued."""
|
||||
update.message.reply_text('Hi!')
|
||||
|
||||
@@ -27,8 +27,8 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Define a few command handlers. These usually take the two arguments bot and
|
||||
# update. Error handlers also receive the raised TelegramError object in error.
|
||||
# Define a few command handlers. These usually take the two arguments update and
|
||||
# context. Error handlers also receive the raised TelegramError object in error.
|
||||
def start(update, context):
|
||||
"""Send a message when the command /start is issued."""
|
||||
update.message.reply_text('Hi!')
|
||||
|
||||
@@ -36,7 +36,7 @@ def start(update, context):
|
||||
# Build InlineKeyboard where each button has a displayed text
|
||||
# and a string as callback_data
|
||||
# The keyboard is a list of button rows, where each row is in turn
|
||||
# a list (henc `[[...]]`).
|
||||
# a list (hence `[[...]]`).
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("1", callback_data=str(ONE)),
|
||||
InlineKeyboardButton("2", callback_data=str(TWO))]
|
||||
@@ -47,7 +47,7 @@ def start(update, context):
|
||||
"Start handler, Choose a route",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
# Tell CosversationHandler that we're in State `FIRST` now
|
||||
# Tell ConversationHandler that we're in state `FIRST` now
|
||||
return FIRST
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ def main():
|
||||
dp = updater.dispatcher
|
||||
|
||||
# Setup conversation handler with the states FIRST and SECOND
|
||||
# Use the pattern parameter to pass CallbackQueryies with specific
|
||||
# Use the pattern parameter to pass CallbackQueries with specific
|
||||
# data pattern to the corresponding handlers.
|
||||
# ^ means "start of line/string"
|
||||
# $ means "end of line/string"
|
||||
@@ -191,7 +191,7 @@ def main():
|
||||
fallbacks=[CommandHandler('start', start)]
|
||||
)
|
||||
|
||||
# Add conversationhandler to dispatcher it will be used for handling
|
||||
# Add ConversationHandler to dispatcher that will be used for handling
|
||||
# updates
|
||||
dp.add_handler(conv_handler)
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 492 KiB |
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
|
||||
"""
|
||||
First, a few callback functions are defined. Then, those functions are passed to
|
||||
the Dispatcher and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
|
||||
Usage:
|
||||
Example of a bot-user conversation using nested ConversationHandlers.
|
||||
Send /start to initiate the conversation.
|
||||
Press Ctrl-C on the command line or send a signal to the process to stop the
|
||||
bot.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from telegram import (InlineKeyboardMarkup, InlineKeyboardButton)
|
||||
from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters,
|
||||
ConversationHandler, CallbackQueryHandler)
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
level=logging.INFO)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# State definitions for top level conversation
|
||||
SELECTING_ACTION, ADDING_MEMBER, ADDING_SELF, DESCRIBING_SELF = map(chr, range(4))
|
||||
# State definitions for second level conversation
|
||||
SELECTING_LEVEL, SELECTING_GENDER = map(chr, range(4, 6))
|
||||
# State definitions for descriptions conversation
|
||||
SELECTING_FEATURE, TYPING = map(chr, range(6, 8))
|
||||
# Meta states
|
||||
STOPPING, SHOWING = map(chr, range(8, 10))
|
||||
# Shortcut for ConversationHandler.END
|
||||
END = ConversationHandler.END
|
||||
|
||||
# Different constants for this example
|
||||
(PARENTS, CHILDREN, SELF, GENDER, MALE, FEMALE, AGE, NAME, START_OVER, FEATURES,
|
||||
CURRENT_FEATURE, CURRENT_LEVEL) = map(chr, range(10, 22))
|
||||
|
||||
|
||||
# Helper
|
||||
def _name_switcher(level):
|
||||
if level == PARENTS:
|
||||
return ('Father', 'Mother')
|
||||
elif level == CHILDREN:
|
||||
return ('Brother', 'Sister')
|
||||
|
||||
|
||||
# Top level conversation callbacks
|
||||
def start(update, context):
|
||||
"""Select an action: Adding parent/child or show data."""
|
||||
text = 'You may add a familiy member, yourself show the gathered data or end the ' \
|
||||
'conversation. To abort, simply type /stop.'
|
||||
buttons = [[
|
||||
InlineKeyboardButton(text='Add family member', callback_data=str(ADDING_MEMBER)),
|
||||
InlineKeyboardButton(text='Add yourself', callback_data=str(ADDING_SELF))
|
||||
], [
|
||||
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
|
||||
InlineKeyboardButton(text='Done', callback_data=str(END))
|
||||
]]
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
|
||||
# If we're starting over we don't need do send a new message
|
||||
if context.user_data.get(START_OVER):
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
else:
|
||||
update.message.reply_text('Hi, I\'m FamiliyBot and here to help you gather information'
|
||||
'about your family.')
|
||||
update.message.reply_text(text=text, reply_markup=keyboard)
|
||||
|
||||
context.user_data[START_OVER] = False
|
||||
return SELECTING_ACTION
|
||||
|
||||
|
||||
def adding_self(update, context):
|
||||
"""Add information about youself."""
|
||||
context.user_data[CURRENT_LEVEL] = SELF
|
||||
text = 'Okay, please tell me about yourself.'
|
||||
button = InlineKeyboardButton(text='Add info', callback_data=str(MALE))
|
||||
keyboard = InlineKeyboardMarkup.from_button(button)
|
||||
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
|
||||
return DESCRIBING_SELF
|
||||
|
||||
|
||||
def show_data(update, context):
|
||||
"""Pretty print gathered data."""
|
||||
def prettyprint(user_data, level):
|
||||
people = user_data.get(level)
|
||||
if not people:
|
||||
return '\nNo information yet.'
|
||||
|
||||
text = ''
|
||||
if level == SELF:
|
||||
for person in user_data[level]:
|
||||
text += '\nName: {0}, Age: {1}'.format(person.get(NAME, '-'), person.get(AGE, '-'))
|
||||
else:
|
||||
male, female = _name_switcher(level)
|
||||
|
||||
for person in user_data[level]:
|
||||
gender = female if person[GENDER] == FEMALE else male
|
||||
text += '\n{0}: Name: {1}, Age: {2}'.format(gender, person.get(NAME, '-'),
|
||||
person.get(AGE, '-'))
|
||||
return text
|
||||
|
||||
ud = context.user_data
|
||||
text = 'Yourself:' + prettyprint(ud, SELF)
|
||||
text += '\n\nParents:' + prettyprint(ud, PARENTS)
|
||||
text += '\n\nChildren:' + prettyprint(ud, CHILDREN)
|
||||
|
||||
buttons = [[
|
||||
InlineKeyboardButton(text='Back', callback_data=str(END))
|
||||
]]
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
ud[START_OVER] = True
|
||||
|
||||
return SHOWING
|
||||
|
||||
|
||||
def stop(update, context):
|
||||
"""End Conversation by command."""
|
||||
update.message.reply_text('Okay, bye.')
|
||||
|
||||
return END
|
||||
|
||||
|
||||
def end(update, context):
|
||||
"""End conversation from InlineKeyboardButton."""
|
||||
text = 'See you around!'
|
||||
update.callback_query.edit_message_text(text=text)
|
||||
|
||||
return END
|
||||
|
||||
|
||||
# Second level conversation callbacks
|
||||
def select_level(update, context):
|
||||
"""Choose to add a parent or a child."""
|
||||
text = 'You may add a parent or a child. Also you can show the gathered data or go back.'
|
||||
buttons = [[
|
||||
InlineKeyboardButton(text='Add parent', callback_data=str(PARENTS)),
|
||||
InlineKeyboardButton(text='Add child', callback_data=str(CHILDREN))
|
||||
], [
|
||||
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
|
||||
InlineKeyboardButton(text='Back', callback_data=str(END))
|
||||
]]
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
|
||||
return SELECTING_LEVEL
|
||||
|
||||
|
||||
def select_gender(update, context):
|
||||
"""Choose to add mother or father."""
|
||||
level = update.callback_query.data
|
||||
context.user_data[CURRENT_LEVEL] = level
|
||||
|
||||
text = 'Please choose, whom to add.'
|
||||
|
||||
male, female = _name_switcher(level)
|
||||
|
||||
buttons = [[
|
||||
InlineKeyboardButton(text='Add ' + male, callback_data=str(MALE)),
|
||||
InlineKeyboardButton(text='Add ' + female, callback_data=str(FEMALE))
|
||||
], [
|
||||
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
|
||||
InlineKeyboardButton(text='Back', callback_data=str(END))
|
||||
]]
|
||||
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
|
||||
return SELECTING_GENDER
|
||||
|
||||
|
||||
def end_second_level(update, context):
|
||||
"""Return to top level conversation."""
|
||||
context.user_data[START_OVER] = True
|
||||
start(update, context)
|
||||
|
||||
return END
|
||||
|
||||
|
||||
# Third level callbacks
|
||||
def select_feature(update, context):
|
||||
"""Select a feature to update for the person."""
|
||||
buttons = [[
|
||||
InlineKeyboardButton(text='Name', callback_data=str(NAME)),
|
||||
InlineKeyboardButton(text='Age', callback_data=str(AGE)),
|
||||
InlineKeyboardButton(text='Done', callback_data=str(END)),
|
||||
]]
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
|
||||
# If we collect features for a new person, clear the cache and save the gender
|
||||
if not context.user_data.get(START_OVER):
|
||||
context.user_data[FEATURES] = {GENDER: update.callback_query.data}
|
||||
text = 'Please select a feature to update.'
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
# But after we do that, we need to send a new message
|
||||
else:
|
||||
text = 'Got it! Please select a feature to update.'
|
||||
update.message.reply_text(text=text, reply_markup=keyboard)
|
||||
|
||||
context.user_data[START_OVER] = False
|
||||
return SELECTING_FEATURE
|
||||
|
||||
|
||||
def ask_for_input(update, context):
|
||||
"""Prompt user to input data for selected feature."""
|
||||
context.user_data[CURRENT_FEATURE] = update.callback_query.data
|
||||
text = 'Okay, tell me.'
|
||||
update.callback_query.edit_message_text(text=text)
|
||||
|
||||
return TYPING
|
||||
|
||||
|
||||
def save_input(update, context):
|
||||
"""Save input for feature and return to feature selection."""
|
||||
ud = context.user_data
|
||||
ud[FEATURES][ud[CURRENT_FEATURE]] = update.message.text
|
||||
|
||||
ud[START_OVER] = True
|
||||
|
||||
return select_feature(update, context)
|
||||
|
||||
|
||||
def end_describing(update, context):
|
||||
"""End gathering of features and return to parent conversation."""
|
||||
ud = context.user_data
|
||||
level = ud[CURRENT_LEVEL]
|
||||
if not ud.get(level):
|
||||
ud[level] = []
|
||||
ud[level].append(ud[FEATURES])
|
||||
|
||||
# Print upper level menu
|
||||
if level == SELF:
|
||||
ud[START_OVER] = True
|
||||
start(update, context)
|
||||
else:
|
||||
select_level(update, context)
|
||||
|
||||
return END
|
||||
|
||||
|
||||
def stop_nested(update, context):
|
||||
"""Completely end conversation from within nested conversation."""
|
||||
update.message.reply_text('Okay, bye.')
|
||||
|
||||
return STOPPING
|
||||
|
||||
|
||||
# Error handler
|
||||
def error(update, context):
|
||||
"""Log Errors caused by Updates."""
|
||||
logger.warning('Update "%s" caused error "%s"', update, context.error)
|
||||
|
||||
|
||||
def main():
|
||||
# Create the Updater and pass it your bot's token.
|
||||
# Make sure to set use_context=True to use the new context based callbacks
|
||||
# Post version 12 this will no longer be necessary
|
||||
updater = Updater("TOKEN", use_context=True)
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dp = updater.dispatcher
|
||||
|
||||
# Set up third level ConversationHandler (collecting features)
|
||||
description_conv = ConversationHandler(
|
||||
entry_points=[CallbackQueryHandler(select_feature,
|
||||
pattern='^' + str(MALE) + '$|^' + str(FEMALE) + '$')],
|
||||
|
||||
states={
|
||||
SELECTING_FEATURE: [CallbackQueryHandler(ask_for_input,
|
||||
pattern='^(?!' + str(END) + ').*$')],
|
||||
TYPING: [MessageHandler(Filters.text, save_input)],
|
||||
},
|
||||
|
||||
fallbacks=[
|
||||
CallbackQueryHandler(end_describing, pattern='^' + str(END) + '$'),
|
||||
CommandHandler('stop', stop_nested)
|
||||
],
|
||||
|
||||
map_to_parent={
|
||||
# Return to second level menu
|
||||
END: SELECTING_LEVEL,
|
||||
# End conversation alltogether
|
||||
STOPPING: STOPPING,
|
||||
}
|
||||
)
|
||||
|
||||
# Set up second level ConversationHandler (adding a person)
|
||||
add_member_conv = ConversationHandler(
|
||||
entry_points=[CallbackQueryHandler(select_level,
|
||||
pattern='^' + str(ADDING_MEMBER) + '$')],
|
||||
|
||||
states={
|
||||
SELECTING_LEVEL: [CallbackQueryHandler(select_gender,
|
||||
pattern='^{0}$|^{1}$'.format(str(PARENTS),
|
||||
str(CHILDREN)))],
|
||||
SELECTING_GENDER: [description_conv]
|
||||
},
|
||||
|
||||
fallbacks=[
|
||||
CallbackQueryHandler(show_data, pattern='^' + str(SHOWING) + '$'),
|
||||
CallbackQueryHandler(end_second_level, pattern='^' + str(END) + '$'),
|
||||
CommandHandler('stop', stop_nested)
|
||||
],
|
||||
|
||||
map_to_parent={
|
||||
# After showing data return to top level menu
|
||||
SHOWING: SHOWING,
|
||||
# Return to top level menu
|
||||
END: SELECTING_ACTION,
|
||||
# End conversation alltogether
|
||||
STOPPING: END,
|
||||
}
|
||||
)
|
||||
|
||||
# Set up top level ConversationHandler (selecting action)
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('start', start)],
|
||||
|
||||
states={
|
||||
SHOWING: [CallbackQueryHandler(start, pattern='^' + str(END) + '$')],
|
||||
SELECTING_ACTION: [
|
||||
add_member_conv,
|
||||
CallbackQueryHandler(show_data, pattern='^' + str(SHOWING) + '$'),
|
||||
CallbackQueryHandler(adding_self, pattern='^' + str(ADDING_SELF) + '$'),
|
||||
CallbackQueryHandler(end, pattern='^' + str(END) + '$'),
|
||||
],
|
||||
DESCRIBING_SELF: [description_conv],
|
||||
},
|
||||
|
||||
fallbacks=[CommandHandler('stop', stop)],
|
||||
)
|
||||
# Because the states of the third level conversation map to the ones of the
|
||||
# second level conversation, we need to be a bit hacky about that:
|
||||
conv_handler.states[SELECTING_LEVEL] = conv_handler.states[SELECTING_ACTION]
|
||||
conv_handler.states[STOPPING] = conv_handler.entry_points
|
||||
|
||||
dp.add_handler(conv_handler)
|
||||
|
||||
# log all errors
|
||||
dp.add_error_handler(error)
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -103,9 +103,9 @@ def precheckout_callback(update, context):
|
||||
query.answer(ok=True)
|
||||
|
||||
|
||||
# finally, after contacting to the payment provider...
|
||||
# finally, after contacting the payment provider...
|
||||
def successful_payment_callback(update, context):
|
||||
# do something after successful receive of payment?
|
||||
# do something after successfully receiving payment?
|
||||
update.message.reply_text("Thank you for your payment!")
|
||||
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Define a few command handlers. These usually take the two arguments bot and
|
||||
# update. Error handlers also receive the raised TelegramError object in error.
|
||||
# Define a few command handlers. These usually take the two arguments update and
|
||||
# context. Error handlers also receive the raised TelegramError object in error.
|
||||
def start(update, context):
|
||||
update.message.reply_text('Hi! Use /set <seconds> to set a timer')
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ beautifulsoup4
|
||||
pytest==4.2.0
|
||||
pytest-timeout
|
||||
wheel
|
||||
attrs==19.1.0
|
||||
|
||||
@@ -14,7 +14,8 @@ upload-dir = docs/build/html
|
||||
|
||||
[flake8]
|
||||
max-line-length = 99
|
||||
ignore = W503
|
||||
ignore = W503, W605
|
||||
exclude = setup.py, docs/source/conf.py
|
||||
|
||||
[yapf]
|
||||
based_on_style = google
|
||||
|
||||
+1
-1
@@ -3439,7 +3439,7 @@ class Bot(TelegramObject):
|
||||
return Poll.de_json(result, self)
|
||||
|
||||
def to_dict(self):
|
||||
data = {'id': self.id, 'username': self.username, 'first_name': self.username}
|
||||
data = {'id': self.id, 'username': self.username, 'first_name': self.first_name}
|
||||
|
||||
if self.last_name:
|
||||
data['last_name'] = self.last_name
|
||||
|
||||
@@ -64,6 +64,20 @@ class ConversationHandler(Handler):
|
||||
To end the conversation, the callback function must return :attr:`END` or ``-1``. To
|
||||
handle the conversation timeout, use handler :attr:`TIMEOUT` or ``-2``.
|
||||
|
||||
Note:
|
||||
In each of the described collections of handlers, a handler may in turn be a
|
||||
:class:`ConversationHandler`. In that case, the nested :class:`ConversationHandler` should
|
||||
have the attribute :attr:`map_to_parent` which allows to return to the parent conversation
|
||||
at specified states within the nested conversation.
|
||||
|
||||
Note that the keys in :attr:`map_to_parent` must not appear as keys in :attr:`states`
|
||||
attribute or else the latter will be ignored. You may map :attr:`END` to one of the parents
|
||||
states to continue the parent conversation after this has ended or even map a state to
|
||||
:attr:`END` to end the *parent* conversation from within the nested one. For an example on
|
||||
nested :class:`ConversationHandler` s, see our `examples`_.
|
||||
|
||||
.. _`examples`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples
|
||||
|
||||
Attributes:
|
||||
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
|
||||
trigger the start of the conversation.
|
||||
@@ -88,6 +102,9 @@ class ConversationHandler(Handler):
|
||||
persistence
|
||||
persistent (:obj:`bool`): Optional. If the conversations dict for this handler should be
|
||||
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
|
||||
map_to_parent (Dict[:obj:`object`, :obj:`object`]): Optional. A :obj:`dict` that can be
|
||||
used to instruct a nested conversationhandler to transition into a mapped state on
|
||||
its parent conversationhandler in place of a specified nested state.
|
||||
|
||||
Args:
|
||||
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
|
||||
@@ -119,6 +136,9 @@ class ConversationHandler(Handler):
|
||||
persistence
|
||||
persistent (:obj:`bool`, optional): If the conversations dict for this handler should be
|
||||
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
|
||||
map_to_parent (Dict[:obj:`object`, :obj:`object`], optional): A :obj:`dict` that can be
|
||||
used to instruct a nested conversationhandler to transition into a mapped state on
|
||||
its parent conversationhandler in place of a specified nested state.
|
||||
|
||||
Raises:
|
||||
ValueError
|
||||
@@ -142,7 +162,8 @@ class ConversationHandler(Handler):
|
||||
per_message=False,
|
||||
conversation_timeout=None,
|
||||
name=None,
|
||||
persistent=False):
|
||||
persistent=False,
|
||||
map_to_parent=None):
|
||||
|
||||
self.entry_points = entry_points
|
||||
self.states = states
|
||||
@@ -160,6 +181,7 @@ class ConversationHandler(Handler):
|
||||
self.persistence = None
|
||||
""":obj:`telegram.ext.BasePersistance`: The persistence used to store conversations.
|
||||
Set by dispatcher"""
|
||||
self.map_to_parent = map_to_parent
|
||||
|
||||
self.timeout_jobs = dict()
|
||||
self.conversations = dict()
|
||||
@@ -328,7 +350,11 @@ class ConversationHandler(Handler):
|
||||
self._trigger_timeout, self.conversation_timeout,
|
||||
context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
|
||||
|
||||
self.update_state(new_state, conversation_key)
|
||||
if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent:
|
||||
self.update_state(self.END, conversation_key)
|
||||
return self.map_to_parent.get(new_state)
|
||||
else:
|
||||
self.update_state(new_state, conversation_key)
|
||||
|
||||
def update_state(self, new_state, key):
|
||||
if new_state == self.END:
|
||||
|
||||
@@ -151,14 +151,11 @@ class Handler(object):
|
||||
optional_args['update_queue'] = dispatcher.update_queue
|
||||
if self.pass_job_queue:
|
||||
optional_args['job_queue'] = dispatcher.job_queue
|
||||
if self.pass_user_data or self.pass_chat_data:
|
||||
chat = update.effective_chat
|
||||
if self.pass_user_data:
|
||||
user = update.effective_user
|
||||
|
||||
if self.pass_user_data:
|
||||
optional_args['user_data'] = dispatcher.user_data[user.id if user else None]
|
||||
|
||||
if self.pass_chat_data:
|
||||
optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None]
|
||||
optional_args['user_data'] = dispatcher.user_data[user.id if user else None]
|
||||
if self.pass_chat_data:
|
||||
chat = update.effective_chat
|
||||
optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None]
|
||||
|
||||
return optional_args
|
||||
|
||||
+1
-1
@@ -17,4 +17,4 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
|
||||
__version__ = '12.1.1'
|
||||
__version__ = '12.2.0'
|
||||
|
||||
+110
-3
@@ -16,8 +16,10 @@
|
||||
#
|
||||
# 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 datetime
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from queue import Queue
|
||||
from threading import Thread, Event
|
||||
@@ -25,8 +27,9 @@ from time import sleep
|
||||
|
||||
import pytest
|
||||
|
||||
from telegram import Bot
|
||||
from telegram.ext import Dispatcher, JobQueue, Updater
|
||||
from telegram import Bot, Message, User, Chat, MessageEntity, Update, \
|
||||
InlineQuery, CallbackQuery, ShippingQuery, PreCheckoutQuery, ChosenInlineResult
|
||||
from telegram.ext import Dispatcher, JobQueue, Updater, BaseFilter
|
||||
from tests.bots import get_bot
|
||||
|
||||
TRAVIS = os.getenv('TRAVIS', False)
|
||||
@@ -46,7 +49,7 @@ def bot_info():
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def bot(bot_info):
|
||||
return Bot(bot_info['token'], private_key=PRIVATE_KEY)
|
||||
return make_bot(bot_info)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@@ -146,3 +149,107 @@ def pytest_configure(config):
|
||||
if sys.version_info >= (3,):
|
||||
config.addinivalue_line('filterwarnings', 'ignore::ResourceWarning')
|
||||
# TODO: Write so good code that we don't need to ignore ResourceWarnings anymore
|
||||
|
||||
|
||||
def make_bot(bot_info):
|
||||
return Bot(bot_info['token'], private_key=PRIVATE_KEY)
|
||||
|
||||
|
||||
CMD_PATTERN = re.compile(r'/[\da-z_]{1,32}(?:@\w{1,32})?')
|
||||
DATE = datetime.datetime.now()
|
||||
|
||||
|
||||
def make_message(text, **kwargs):
|
||||
"""
|
||||
Testing utility factory to create a fake ``telegram.Message`` with
|
||||
reasonable defaults for mimicking a real message.
|
||||
:param text: (str) message text
|
||||
:return: a (fake) ``telegram.Message``
|
||||
"""
|
||||
return Message(message_id=1,
|
||||
from_user=kwargs.pop('user', User(id=1, first_name='', is_bot=False)),
|
||||
date=kwargs.pop('date', DATE),
|
||||
chat=kwargs.pop('chat', Chat(id=1, type='')),
|
||||
text=text,
|
||||
bot=kwargs.pop('bot', make_bot(get_bot())),
|
||||
**kwargs)
|
||||
|
||||
|
||||
def make_command_message(text, **kwargs):
|
||||
"""
|
||||
Testing utility factory to create a message containing a single telegram
|
||||
command.
|
||||
Mimics the Telegram API in that it identifies commands within the message
|
||||
and tags the returned ``Message`` object with the appropriate ``MessageEntity``
|
||||
tag (but it does this only for commands).
|
||||
|
||||
:param text: (str) message text containing (or not) the command
|
||||
:return: a (fake) ``telegram.Message`` containing only the command
|
||||
"""
|
||||
|
||||
match = re.search(CMD_PATTERN, text)
|
||||
entities = [MessageEntity(type=MessageEntity.BOT_COMMAND,
|
||||
offset=match.start(0),
|
||||
length=len(match.group(0)))] if match else []
|
||||
|
||||
return make_message(text, entities=entities, **kwargs)
|
||||
|
||||
|
||||
def make_message_update(message, message_factory=make_message, edited=False, **kwargs):
|
||||
"""
|
||||
Testing utility factory to create an update from a message, as either a
|
||||
``telegram.Message`` or a string. In the latter case ``message_factory``
|
||||
is used to convert ``message`` to a ``telegram.Message``.
|
||||
:param message: either a ``telegram.Message`` or a string with the message text
|
||||
:param message_factory: function to convert the message text into a ``telegram.Message``
|
||||
:param edited: whether the message should be stored as ``edited_message`` (vs. ``message``)
|
||||
:return: ``telegram.Update`` with the given message
|
||||
"""
|
||||
if not isinstance(message, Message):
|
||||
message = message_factory(message, **kwargs)
|
||||
update_kwargs = {'message' if not edited else 'edited_message': message}
|
||||
return Update(0, **update_kwargs)
|
||||
|
||||
|
||||
def make_command_update(message, edited=False, **kwargs):
|
||||
"""
|
||||
Testing utility factory to create an update from a message that potentially
|
||||
contains a command. See ``make_command_message`` for more details.
|
||||
:param message: message potentially containing a command
|
||||
:param edited: whether the message should be stored as ``edited_message`` (vs. ``message``)
|
||||
:return: ``telegram.Update`` with the given message
|
||||
"""
|
||||
return make_message_update(message, make_command_message, edited, **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def mock_filter():
|
||||
class MockFilter(BaseFilter):
|
||||
def __init__(self):
|
||||
self.tested = False
|
||||
|
||||
def filter(self, message):
|
||||
self.tested = True
|
||||
|
||||
return MockFilter()
|
||||
|
||||
|
||||
def get_false_update_fixture_decorator_params():
|
||||
message = Message(1, User(1, '', False), DATE, Chat(1, ''), text='test')
|
||||
params = [
|
||||
{'callback_query': CallbackQuery(1, User(1, '', False), 'chat', message=message)},
|
||||
{'channel_post': message},
|
||||
{'edited_channel_post': message},
|
||||
{'inline_query': InlineQuery(1, User(1, '', False), '', '')},
|
||||
{'chosen_inline_result': ChosenInlineResult('id', User(1, '', False), '')},
|
||||
{'shipping_query': ShippingQuery('id', User(1, '', False), '', None)},
|
||||
{'pre_checkout_query': PreCheckoutQuery('id', User(1, '', False), '', 0, '')},
|
||||
{'callback_query': CallbackQuery(1, User(1, '', False), 'chat')}
|
||||
]
|
||||
ids = tuple(key for kwargs in params for key in kwargs)
|
||||
return {'params': params, 'ids': ids}
|
||||
|
||||
|
||||
@pytest.fixture(scope='function', **get_false_update_fixture_decorator_params())
|
||||
def false_update(request):
|
||||
return Update(update_id=1, **request.param)
|
||||
|
||||
@@ -89,6 +89,18 @@ class TestBot(object):
|
||||
assert get_me_bot.last_name == bot.last_name
|
||||
assert get_me_bot.name == bot.name
|
||||
|
||||
@flaky(3, 1)
|
||||
@pytest.mark.timeout(10)
|
||||
def test_to_dict(self, bot):
|
||||
to_dict_bot = bot.to_dict()
|
||||
|
||||
assert isinstance(to_dict_bot, dict)
|
||||
assert to_dict_bot["id"] == bot.id
|
||||
assert to_dict_bot["username"] == bot.username
|
||||
assert to_dict_bot["first_name"] == bot.first_name
|
||||
if bot.last_name:
|
||||
assert to_dict_bot["last_name"] == bot.last_name
|
||||
|
||||
@flaky(3, 1)
|
||||
@pytest.mark.timeout(10)
|
||||
def test_forward_message(self, bot, chat_id, message):
|
||||
|
||||
+263
-535
@@ -21,50 +21,30 @@ import re
|
||||
from queue import Queue
|
||||
|
||||
import pytest
|
||||
import itertools
|
||||
from telegram.utils.deprecate import TelegramDeprecationWarning
|
||||
|
||||
from telegram import (Message, Update, Chat, Bot, User, CallbackQuery, InlineQuery,
|
||||
ChosenInlineResult, ShippingQuery, PreCheckoutQuery, MessageEntity)
|
||||
from telegram.ext import CommandHandler, Filters, BaseFilter, CallbackContext, JobQueue, \
|
||||
PrefixHandler
|
||||
|
||||
message = Message(1, User(1, '', False), None, Chat(1, ''), text='test')
|
||||
|
||||
params = [
|
||||
{'callback_query': CallbackQuery(1, User(1, '', False), 'chat', message=message)},
|
||||
{'channel_post': message},
|
||||
{'edited_channel_post': message},
|
||||
{'inline_query': InlineQuery(1, User(1, '', False), '', '')},
|
||||
{'chosen_inline_result': ChosenInlineResult('id', User(1, '', False), '')},
|
||||
{'shipping_query': ShippingQuery('id', User(1, '', False), '', None)},
|
||||
{'pre_checkout_query': PreCheckoutQuery('id', User(1, '', False), '', 0, '')},
|
||||
{'callback_query': CallbackQuery(1, User(1, '', False), 'chat')}
|
||||
]
|
||||
|
||||
ids = ('callback_query', 'channel_post', 'edited_channel_post', 'inline_query',
|
||||
'chosen_inline_result', 'shipping_query', 'pre_checkout_query',
|
||||
'callback_query_without_message',)
|
||||
from telegram import Message, Update, Chat, Bot
|
||||
from telegram.ext import CommandHandler, Filters, CallbackContext, JobQueue, PrefixHandler
|
||||
from tests.conftest import make_command_message, make_command_update, make_message, \
|
||||
make_message_update
|
||||
|
||||
|
||||
@pytest.fixture(scope='class', params=params, ids=ids)
|
||||
def false_update(request):
|
||||
return Update(update_id=1, **request.param)
|
||||
def is_match(handler, update):
|
||||
"""
|
||||
Utility function that returns whether an update matched
|
||||
against a specific handler.
|
||||
:param handler: ``CommandHandler`` to check against
|
||||
:param update: update to check
|
||||
:return: (bool) whether ``update`` matched with ``handler``
|
||||
"""
|
||||
check = handler.check_update(update)
|
||||
return check is not None and check is not False
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def message(bot):
|
||||
return Message(message_id=1,
|
||||
from_user=User(id=1, first_name='', is_bot=False),
|
||||
date=None,
|
||||
chat=Chat(id=1, type=''),
|
||||
message='/test',
|
||||
bot=bot,
|
||||
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
|
||||
offset=0,
|
||||
length=len('/test'))])
|
||||
|
||||
|
||||
class TestCommandHandler(object):
|
||||
class BaseTest(object):
|
||||
"""Base class for command and prefix handler test classes. Contains
|
||||
utility methods an several callbacks used by both classes."""
|
||||
test_flag = False
|
||||
SRE_TYPE = type(re.match("", ""))
|
||||
|
||||
@@ -72,30 +52,33 @@ class TestCommandHandler(object):
|
||||
def reset(self):
|
||||
self.test_flag = False
|
||||
|
||||
PASS_KEYWORDS = ('pass_user_data', 'pass_chat_data', 'pass_job_queue', 'pass_update_queue')
|
||||
|
||||
@pytest.fixture(scope='module', params=itertools.combinations(PASS_KEYWORDS, 2))
|
||||
def pass_combination(self, request):
|
||||
return {key: True for key in request.param}
|
||||
|
||||
def response(self, dispatcher, update):
|
||||
"""
|
||||
Utility to send an update to a dispatcher and assert
|
||||
whether the callback was called appropriately. Its purpose is
|
||||
for repeated usage in the same test function.
|
||||
"""
|
||||
self.test_flag = False
|
||||
dispatcher.process_update(update)
|
||||
return self.test_flag
|
||||
|
||||
def callback_basic(self, bot, update):
|
||||
test_bot = isinstance(bot, Bot)
|
||||
test_update = isinstance(update, Update)
|
||||
self.test_flag = test_bot and test_update
|
||||
|
||||
def callback_data_1(self, bot, update, user_data=None, chat_data=None):
|
||||
self.test_flag = (user_data is not None) or (chat_data is not None)
|
||||
def make_callback_for(self, pass_keyword):
|
||||
def callback(bot, update, **kwargs):
|
||||
self.test_flag = kwargs.get(keyword, None) is not None
|
||||
|
||||
def callback_data_2(self, bot, update, user_data=None, chat_data=None):
|
||||
self.test_flag = (user_data is not None) and (chat_data is not None)
|
||||
|
||||
def callback_queue_1(self, bot, update, job_queue=None, update_queue=None):
|
||||
self.test_flag = (job_queue is not None) or (update_queue is not None)
|
||||
|
||||
def callback_queue_2(self, bot, update, job_queue=None, update_queue=None):
|
||||
self.test_flag = (job_queue is not None) and (update_queue is not None)
|
||||
|
||||
def ch_callback_args(self, bot, update, args):
|
||||
if update.message.text == '/test':
|
||||
self.test_flag = len(args) == 0
|
||||
elif update.message.text == '/test@{}'.format(bot.username):
|
||||
self.test_flag = len(args) == 0
|
||||
else:
|
||||
self.test_flag = args == ['one', 'two']
|
||||
keyword = pass_keyword[5:]
|
||||
return callback
|
||||
|
||||
def callback_context(self, update, context):
|
||||
self.test_flag = (isinstance(context, CallbackContext)
|
||||
@@ -122,550 +105,295 @@ class TestCommandHandler(object):
|
||||
num = len(context.matches) == 2
|
||||
self.test_flag = types and num
|
||||
|
||||
def test_basic(self, dp, message):
|
||||
handler = CommandHandler('test', self.callback_basic)
|
||||
def _test_context_args_or_regex(self, cdp, handler, text):
|
||||
cdp.add_handler(handler)
|
||||
update = make_command_update(text)
|
||||
assert not self.response(cdp, update)
|
||||
update.message.text += ' one two'
|
||||
assert self.response(cdp, update)
|
||||
|
||||
def _test_edited(self, message, handler_edited, handler_not_edited):
|
||||
"""
|
||||
Assert whether a handler that should accept edited messages
|
||||
and a handler that shouldn't work correctly.
|
||||
:param message: ``telegram.Message`` to check against the handlers
|
||||
:param handler_edited: handler that should accept edited messages
|
||||
:param handler_not_edited: handler that should not accept edited messages
|
||||
"""
|
||||
update = make_command_update(message)
|
||||
edited_update = make_command_update(message, edited=True)
|
||||
|
||||
assert is_match(handler_edited, update)
|
||||
assert is_match(handler_edited, edited_update)
|
||||
assert is_match(handler_not_edited, update)
|
||||
assert not is_match(handler_not_edited, edited_update)
|
||||
|
||||
|
||||
# ----------------------------- CommandHandler -----------------------------
|
||||
|
||||
class TestCommandHandler(BaseTest):
|
||||
CMD = '/test'
|
||||
|
||||
@pytest.fixture(scope='class')
|
||||
def command(self):
|
||||
return self.CMD
|
||||
|
||||
@pytest.fixture(scope='class')
|
||||
def command_message(self, command):
|
||||
return make_command_message(command)
|
||||
|
||||
@pytest.fixture(scope='class')
|
||||
def command_update(self, command_message):
|
||||
return make_command_update(command_message)
|
||||
|
||||
def ch_callback_args(self, bot, update, args):
|
||||
if update.message.text == self.CMD:
|
||||
self.test_flag = len(args) == 0
|
||||
elif update.message.text == '{}@{}'.format(self.CMD, bot.username):
|
||||
self.test_flag = len(args) == 0
|
||||
else:
|
||||
self.test_flag = args == ['one', 'two']
|
||||
|
||||
def make_default_handler(self, callback=None, **kwargs):
|
||||
callback = callback or self.callback_basic
|
||||
return CommandHandler(self.CMD[1:], callback, **kwargs)
|
||||
|
||||
def test_basic(self, dp, command):
|
||||
"""Test whether a command handler responds to its command
|
||||
and not to others, or badly formatted commands"""
|
||||
handler = self.make_default_handler()
|
||||
dp.add_handler(handler)
|
||||
|
||||
message.text = '/test'
|
||||
dp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
assert self.response(dp, make_command_update(command))
|
||||
assert not is_match(handler, make_command_update(command[1:]))
|
||||
assert not is_match(handler, make_command_update('/not{}'.format(command[1:])))
|
||||
assert not is_match(handler, make_command_update('not {} at start'.format(command)))
|
||||
|
||||
message.text = '/nottest'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
message.text = 'test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
message.text = 'not /test at start'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
message.entities = []
|
||||
message.text = '/test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
@pytest.mark.parametrize('command',
|
||||
@pytest.mark.parametrize('cmd',
|
||||
['way_too_longcommand1234567yes_way_toooooooLong', 'ïñválídletters',
|
||||
'invalid #&* chars'],
|
||||
ids=['too long', 'invalid letter', 'invalid characters'])
|
||||
def test_invalid_commands(self, command):
|
||||
def test_invalid_commands(self, cmd):
|
||||
with pytest.raises(ValueError, match='not a valid bot command'):
|
||||
CommandHandler(command, self.callback_basic)
|
||||
CommandHandler(cmd, self.callback_basic)
|
||||
|
||||
def test_command_list(self, message):
|
||||
def test_command_list(self):
|
||||
"""A command handler with multiple commands registered should respond to all of them."""
|
||||
handler = CommandHandler(['test', 'star'], self.callback_basic)
|
||||
|
||||
message.text = '/test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
|
||||
message.text = '/star'
|
||||
check = handler.check_update(Update(0, message))
|
||||
|
||||
message.text = '/stop'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
assert is_match(handler, make_command_update('/test'))
|
||||
assert is_match(handler, make_command_update('/star'))
|
||||
assert not is_match(handler, make_command_update('/stop'))
|
||||
|
||||
def test_deprecation_warning(self):
|
||||
"""``allow_edited`` deprecated in favor of filters"""
|
||||
with pytest.warns(TelegramDeprecationWarning, match='See https://git.io/fxJuV'):
|
||||
CommandHandler('test', self.callback_basic, allow_edited=True)
|
||||
self.make_default_handler(allow_edited=True)
|
||||
|
||||
def test_no_edited(self, message):
|
||||
handler = CommandHandler('test', self.callback_basic)
|
||||
message.text = '/test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
def test_edited(self, command_message):
|
||||
"""Test that a CH responds to an edited message iff its filters allow it"""
|
||||
handler_edited = self.make_default_handler()
|
||||
handler_no_edited = self.make_default_handler(filters=~Filters.update.edited_message)
|
||||
self._test_edited(command_message, handler_edited, handler_no_edited)
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=message))
|
||||
assert check is not None and check is not False
|
||||
def test_edited_deprecated(self, command_message):
|
||||
"""Test that a CH responds to an edited message iff ``allow_edited`` is True"""
|
||||
handler_edited = self.make_default_handler(allow_edited=True)
|
||||
handler_no_edited = self.make_default_handler(allow_edited=False)
|
||||
self._test_edited(command_message, handler_edited, handler_no_edited)
|
||||
|
||||
handler = CommandHandler('test', self.callback_basic,
|
||||
filters=~Filters.update.edited_message)
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
def test_directed_commands(self, bot, command):
|
||||
"""Test recognition of commands with a mention to the bot"""
|
||||
handler = self.make_default_handler()
|
||||
assert is_match(handler, make_command_update(command + '@' + bot.username, bot=bot))
|
||||
assert not is_match(handler, make_command_update(command + '@otherbot', bot=bot))
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=message))
|
||||
assert check is None or check is False
|
||||
def test_with_filter(self, command):
|
||||
"""Test that a CH with a (generic) filter responds iff its filters match"""
|
||||
handler = self.make_default_handler(filters=Filters.group)
|
||||
assert is_match(handler, make_command_update(command, chat=Chat(-23, Chat.GROUP)))
|
||||
assert not is_match(handler, make_command_update(command, chat=Chat(23, Chat.PRIVATE)))
|
||||
|
||||
def test_edited_deprecated(self, message):
|
||||
handler = CommandHandler('test', self.callback_basic,
|
||||
allow_edited=False)
|
||||
message.text = '/test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=message))
|
||||
assert check is None or check is False
|
||||
|
||||
handler = CommandHandler('test', self.callback_basic,
|
||||
allow_edited=True)
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
def test_directed_commands(self, message):
|
||||
handler = CommandHandler('test', self.callback_basic)
|
||||
|
||||
message.text = '/test@{}'.format(message.bot.username)
|
||||
message.entities[0].length = len(message.text)
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
message.text = '/test@otherbot'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
def test_with_filter(self, message):
|
||||
handler = CommandHandler('test', self.callback_basic, Filters.group)
|
||||
|
||||
message.chat = Chat(-23, 'group')
|
||||
message.text = '/test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
message.chat = Chat(23, 'private')
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
def test_pass_args(self, dp, message):
|
||||
handler = CommandHandler('test', self.ch_callback_args, pass_args=True)
|
||||
def test_pass_args(self, dp, bot, command):
|
||||
"""Test the passing of arguments alongside a command"""
|
||||
handler = self.make_default_handler(self.ch_callback_args, pass_args=True)
|
||||
dp.add_handler(handler)
|
||||
at_command = '{}@{}'.format(command, bot.username)
|
||||
assert self.response(dp, make_command_update(command))
|
||||
assert self.response(dp, make_command_update(command + ' one two'))
|
||||
assert self.response(dp, make_command_update(at_command, bot=bot))
|
||||
assert self.response(dp, make_command_update(at_command + ' one two', bot=bot))
|
||||
|
||||
message.text = '/test'
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
self.test_flag = False
|
||||
message.text = '/test@{}'.format(message.bot.username)
|
||||
message.entities[0].length = len(message.text)
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
self.test_flag = False
|
||||
message.text = '/test@{} one two'.format(message.bot.username)
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
self.test_flag = False
|
||||
message.text = '/test one two'
|
||||
message.entities[0].length = len('/test')
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
def test_newline(self, dp, message):
|
||||
handler = CommandHandler('test', self.callback_basic)
|
||||
def test_newline(self, dp, command):
|
||||
"""Assert that newlines don't interfere with a command handler matching a message"""
|
||||
handler = self.make_default_handler()
|
||||
dp.add_handler(handler)
|
||||
update = make_command_update(command + '\nfoobar')
|
||||
assert is_match(handler, update)
|
||||
assert self.response(dp, update)
|
||||
|
||||
message.text = '/test\nfoobar'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
dp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
|
||||
def test_pass_user_or_chat_data(self, dp, message):
|
||||
handler = CommandHandler('test', self.callback_data_1,
|
||||
pass_user_data=True)
|
||||
@pytest.mark.parametrize('pass_keyword', BaseTest.PASS_KEYWORDS)
|
||||
def test_pass_data(self, dp, command_update, pass_combination, pass_keyword):
|
||||
handler = CommandHandler('test', self.make_callback_for(pass_keyword), **pass_combination)
|
||||
dp.add_handler(handler)
|
||||
|
||||
message.text = '/test'
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
handler = CommandHandler('test', self.callback_data_1,
|
||||
pass_chat_data=True)
|
||||
dp.add_handler(handler)
|
||||
|
||||
self.test_flag = False
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
handler = CommandHandler('test', self.callback_data_2,
|
||||
pass_chat_data=True,
|
||||
pass_user_data=True)
|
||||
dp.add_handler(handler)
|
||||
|
||||
self.test_flag = False
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
def test_pass_job_or_update_queue(self, dp, message):
|
||||
handler = CommandHandler('test', self.callback_queue_1,
|
||||
pass_job_queue=True)
|
||||
dp.add_handler(handler)
|
||||
|
||||
message.text = '/test'
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
handler = CommandHandler('test', self.callback_queue_1,
|
||||
pass_update_queue=True)
|
||||
dp.add_handler(handler)
|
||||
|
||||
self.test_flag = False
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
handler = CommandHandler('test', self.callback_queue_2,
|
||||
pass_job_queue=True,
|
||||
pass_update_queue=True)
|
||||
dp.add_handler(handler)
|
||||
|
||||
self.test_flag = False
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
assert self.response(dp, command_update) == pass_combination.get(pass_keyword, False)
|
||||
|
||||
def test_other_update_types(self, false_update):
|
||||
handler = CommandHandler('test', self.callback_basic)
|
||||
check = handler.check_update(false_update)
|
||||
assert check is None or check is False
|
||||
"""Test that a command handler doesn't respond to unrelated updates"""
|
||||
handler = self.make_default_handler()
|
||||
assert not is_match(handler, false_update)
|
||||
|
||||
def test_filters_for_wrong_command(self, message):
|
||||
def test_filters_for_wrong_command(self, mock_filter):
|
||||
"""Filters should not be executed if the command does not match the handler"""
|
||||
handler = self.make_default_handler(filters=mock_filter)
|
||||
assert not is_match(handler, make_command_update('/star'))
|
||||
assert not mock_filter.tested
|
||||
|
||||
class TestFilter(BaseFilter):
|
||||
def __init__(self):
|
||||
self.tested = False
|
||||
|
||||
def filter(self, message):
|
||||
self.tested = True
|
||||
|
||||
test_filter = TestFilter()
|
||||
|
||||
handler = CommandHandler('test', self.callback_basic,
|
||||
filters=test_filter)
|
||||
message.text = '/star'
|
||||
|
||||
check = handler.check_update(Update(0, message=message))
|
||||
assert check is None or check is False
|
||||
|
||||
assert not test_filter.tested
|
||||
|
||||
def test_context(self, cdp, message):
|
||||
handler = CommandHandler('test', self.callback_context)
|
||||
def test_context(self, cdp, command_update):
|
||||
"""Test correct behaviour of CHs with context-based callbacks"""
|
||||
handler = self.make_default_handler(self.callback_context)
|
||||
cdp.add_handler(handler)
|
||||
assert self.response(cdp, command_update)
|
||||
|
||||
message.text = '/test'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
def test_context_args(self, cdp, command):
|
||||
"""Test CHs that pass arguments through ``context``"""
|
||||
handler = self.make_default_handler(self.callback_context_args)
|
||||
self._test_context_args_or_regex(cdp, handler, command)
|
||||
|
||||
def test_context_args(self, cdp, message):
|
||||
handler = CommandHandler('test', self.callback_context_args)
|
||||
cdp.add_handler(handler)
|
||||
def test_context_regex(self, cdp, command):
|
||||
"""Test CHs with context-based callbacks and a single filter"""
|
||||
handler = self.make_default_handler(self.callback_context_regex1,
|
||||
filters=Filters.regex('one two'))
|
||||
self._test_context_args_or_regex(cdp, handler, command)
|
||||
|
||||
message.text = '/test'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert not self.test_flag
|
||||
|
||||
message.text = '/test one two'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
|
||||
def test_context_regex(self, cdp, message):
|
||||
handler = CommandHandler('test', self.callback_context_regex1, Filters.regex('one two'))
|
||||
cdp.add_handler(handler)
|
||||
|
||||
message.text = '/test'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert not self.test_flag
|
||||
|
||||
message.text += ' one two'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
|
||||
def test_context_multiple_regex(self, cdp, message):
|
||||
handler = CommandHandler('test', self.callback_context_regex2,
|
||||
Filters.regex('one') & Filters.regex('two'))
|
||||
cdp.add_handler(handler)
|
||||
|
||||
message.text = '/test'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert not self.test_flag
|
||||
|
||||
message.text += ' one two'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
def test_context_multiple_regex(self, cdp, command):
|
||||
"""Test CHs with context-based callbacks and filters combined"""
|
||||
handler = self.make_default_handler(self.callback_context_regex2,
|
||||
filters=Filters.regex('one') & Filters.regex('two'))
|
||||
self._test_context_args_or_regex(cdp, handler, command)
|
||||
|
||||
|
||||
par = ['!help', '!test', '#help', '#test', 'mytrig-help', 'mytrig-test']
|
||||
# ----------------------------- PrefixHandler -----------------------------
|
||||
|
||||
def combinations(prefixes, commands):
|
||||
return (prefix + command for prefix in prefixes for command in commands)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function', params=par)
|
||||
def prefixmessage(bot, request):
|
||||
return Message(message_id=1,
|
||||
from_user=User(id=1, first_name='', is_bot=False),
|
||||
date=None,
|
||||
chat=Chat(id=1, type=''),
|
||||
text=request.param,
|
||||
bot=bot)
|
||||
class TestPrefixHandler(BaseTest):
|
||||
# Prefixes and commands with which to test PrefixHandler:
|
||||
PREFIXES = ['!', '#', 'mytrig-']
|
||||
COMMANDS = ['help', 'test']
|
||||
COMBINATIONS = list(combinations(PREFIXES, COMMANDS))
|
||||
|
||||
@pytest.fixture(scope='class', params=PREFIXES)
|
||||
def prefix(self, request):
|
||||
return request.param
|
||||
|
||||
class TestPrefixHandler(object):
|
||||
test_flag = False
|
||||
SRE_TYPE = type(re.match("", ""))
|
||||
@pytest.fixture(scope='class', params=[1, 2], ids=['single prefix', 'multiple prefixes'])
|
||||
def prefixes(self, request):
|
||||
return TestPrefixHandler.PREFIXES[:request.param]
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset(self):
|
||||
self.test_flag = False
|
||||
@pytest.fixture(scope='class', params=COMMANDS)
|
||||
def command(self, request):
|
||||
return request.param
|
||||
|
||||
def callback_basic(self, bot, update):
|
||||
test_bot = isinstance(bot, Bot)
|
||||
test_update = isinstance(update, Update)
|
||||
self.test_flag = test_bot and test_update
|
||||
@pytest.fixture(scope='class', params=[1, 2], ids=['single command', 'multiple commands'])
|
||||
def commands(self, request):
|
||||
return TestPrefixHandler.COMMANDS[:request.param]
|
||||
|
||||
def callback_data_1(self, bot, update, user_data=None, chat_data=None):
|
||||
self.test_flag = (user_data is not None) or (chat_data is not None)
|
||||
@pytest.fixture(scope='class')
|
||||
def prefix_message_text(self, prefix, command):
|
||||
return prefix + command
|
||||
|
||||
def callback_data_2(self, bot, update, user_data=None, chat_data=None):
|
||||
self.test_flag = (user_data is not None) and (chat_data is not None)
|
||||
@pytest.fixture(scope='class')
|
||||
def prefix_message(self, prefix_message_text):
|
||||
return make_message(prefix_message_text)
|
||||
|
||||
def callback_queue_1(self, bot, update, job_queue=None, update_queue=None):
|
||||
self.test_flag = (job_queue is not None) or (update_queue is not None)
|
||||
@pytest.fixture(scope='class')
|
||||
def prefix_message_update(self, prefix_message):
|
||||
return make_message_update(prefix_message)
|
||||
|
||||
def callback_queue_2(self, bot, update, job_queue=None, update_queue=None):
|
||||
self.test_flag = (job_queue is not None) and (update_queue is not None)
|
||||
def make_default_handler(self, callback=None, **kwargs):
|
||||
callback = callback or self.callback_basic
|
||||
return PrefixHandler(self.PREFIXES, self.COMMANDS, callback, **kwargs)
|
||||
|
||||
def ch_callback_args(self, bot, update, args):
|
||||
if update.message.text in par:
|
||||
if update.message.text in TestPrefixHandler.COMBINATIONS:
|
||||
self.test_flag = len(args) == 0
|
||||
else:
|
||||
self.test_flag = args == ['one', 'two']
|
||||
|
||||
def callback_context(self, update, context):
|
||||
self.test_flag = (isinstance(context, CallbackContext)
|
||||
and isinstance(context.bot, Bot)
|
||||
and isinstance(update, Update)
|
||||
and isinstance(context.update_queue, Queue)
|
||||
and isinstance(context.job_queue, JobQueue)
|
||||
and isinstance(context.user_data, dict)
|
||||
and isinstance(context.chat_data, dict)
|
||||
and isinstance(update.message, Message))
|
||||
|
||||
def callback_context_args(self, update, context):
|
||||
self.test_flag = context.args == ['one', 'two']
|
||||
|
||||
def callback_context_regex1(self, update, context):
|
||||
if context.matches:
|
||||
types = all([type(res) == self.SRE_TYPE for res in context.matches])
|
||||
num = len(context.matches) == 1
|
||||
self.test_flag = types and num
|
||||
|
||||
def callback_context_regex2(self, update, context):
|
||||
if context.matches:
|
||||
types = all([type(res) == self.SRE_TYPE for res in context.matches])
|
||||
num = len(context.matches) == 2
|
||||
self.test_flag = types and num
|
||||
|
||||
def test_basic(self, dp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
|
||||
def test_basic(self, dp, prefix, command):
|
||||
"""Test the basic expected response from a prefix handler"""
|
||||
handler = self.make_default_handler()
|
||||
dp.add_handler(handler)
|
||||
text = prefix + command
|
||||
|
||||
dp.process_update(Update(0, prefixmessage))
|
||||
assert self.test_flag
|
||||
assert self.response(dp, make_message_update(text))
|
||||
assert not is_match(handler, make_message_update(command))
|
||||
assert not is_match(handler, make_message_update(prefix + 'notacommand'))
|
||||
assert not is_match(handler, make_command_update('not {} at start'.format(text)))
|
||||
|
||||
prefixmessage.text = 'test'
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is None or check is False
|
||||
def test_single_multi_prefixes_commands(self, prefixes, commands, prefix_message_update):
|
||||
"""Test various combinations of prefixes and commands"""
|
||||
handler = self.make_default_handler()
|
||||
result = is_match(handler, prefix_message_update)
|
||||
expected = prefix_message_update.message.text in combinations(prefixes, commands)
|
||||
return result == expected
|
||||
|
||||
prefixmessage.text = '#nocom'
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is None or check is False
|
||||
def test_edited(self, prefix_message):
|
||||
handler_edited = self.make_default_handler()
|
||||
handler_no_edited = self.make_default_handler(filters=~Filters.update.edited_message)
|
||||
self._test_edited(prefix_message, handler_edited, handler_no_edited)
|
||||
|
||||
message.text = 'not !test at start'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
def test_with_filter(self, prefix_message_text):
|
||||
handler = self.make_default_handler(filters=Filters.group)
|
||||
text = prefix_message_text
|
||||
assert is_match(handler, make_message_update(text, chat=Chat(-23, Chat.GROUP)))
|
||||
assert not is_match(handler, make_message_update(text, chat=Chat(23, Chat.PRIVATE)))
|
||||
|
||||
def test_single_prefix_single_command(self, prefixmessage):
|
||||
handler = PrefixHandler('!', 'test', self.callback_basic)
|
||||
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
if prefixmessage.text in ['!test']:
|
||||
assert check is not None and check is not False
|
||||
else:
|
||||
assert check is None or check is False
|
||||
|
||||
def test_single_prefix_multi_command(self, prefixmessage):
|
||||
handler = PrefixHandler('!', ['test', 'help'], self.callback_basic)
|
||||
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
if prefixmessage.text in ['!test', '!help']:
|
||||
assert check is not None and check is not False
|
||||
else:
|
||||
assert check is None or check is False
|
||||
|
||||
def test_multi_prefix_single_command(self, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#'], 'test', self.callback_basic)
|
||||
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
if prefixmessage.text in ['!test', '#test']:
|
||||
assert check is not None and check is not False
|
||||
else:
|
||||
assert check is None or check is False
|
||||
|
||||
def test_no_edited(self, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is not None and check is not False
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=prefixmessage))
|
||||
assert check is not None and check is not False
|
||||
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
|
||||
filters=~Filters.update.edited_message)
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is not None and check is not False
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=prefixmessage))
|
||||
assert check is None or check is False
|
||||
|
||||
def test_with_filter(self, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
|
||||
filters=Filters.group)
|
||||
|
||||
prefixmessage.chat = Chat(-23, 'group')
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is not None and check is not False
|
||||
|
||||
prefixmessage.chat = Chat(23, 'private')
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is None or check is False
|
||||
|
||||
def test_pass_args(self, dp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.ch_callback_args,
|
||||
pass_args=True)
|
||||
def test_pass_args(self, dp, prefix_message):
|
||||
handler = self.make_default_handler(self.ch_callback_args, pass_args=True)
|
||||
dp.add_handler(handler)
|
||||
assert self.response(dp, make_message_update(prefix_message))
|
||||
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
update_with_args = make_message_update(prefix_message.text + ' one two')
|
||||
assert self.response(dp, update_with_args)
|
||||
|
||||
self.test_flag = False
|
||||
prefixmessage.text += ' one two'
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
def test_pass_user_or_chat_data(self, dp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_1,
|
||||
pass_user_data=True)
|
||||
@pytest.mark.parametrize('pass_keyword', BaseTest.PASS_KEYWORDS)
|
||||
def test_pass_data(self, dp, pass_combination, prefix_message_update, pass_keyword):
|
||||
"""Assert that callbacks receive data iff its corresponding ``pass_*`` kwarg is enabled"""
|
||||
handler = self.make_default_handler(self.make_callback_for(pass_keyword),
|
||||
**pass_combination)
|
||||
dp.add_handler(handler)
|
||||
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
self.test_flag = False
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_1,
|
||||
pass_chat_data=True)
|
||||
dp.add_handler(handler)
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
self.test_flag = False
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_2,
|
||||
pass_chat_data=True, pass_user_data=True)
|
||||
dp.add_handler(handler)
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
def test_pass_job_or_update_queue(self, dp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_1,
|
||||
pass_job_queue=True)
|
||||
dp.add_handler(handler)
|
||||
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
self.test_flag = False
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_1,
|
||||
pass_update_queue=True)
|
||||
dp.add_handler(handler)
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
self.test_flag = False
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_2,
|
||||
pass_job_queue=True, pass_update_queue=True)
|
||||
dp.add_handler(handler)
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
assert self.response(dp, prefix_message_update) \
|
||||
== pass_combination.get(pass_keyword, False)
|
||||
|
||||
def test_other_update_types(self, false_update):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
|
||||
check = handler.check_update(false_update)
|
||||
assert check is None or check is False
|
||||
handler = self.make_default_handler()
|
||||
assert not is_match(handler, false_update)
|
||||
|
||||
def test_filters_for_wrong_command(self, prefixmessage):
|
||||
def test_filters_for_wrong_command(self, mock_filter):
|
||||
"""Filters should not be executed if the command does not match the handler"""
|
||||
handler = self.make_default_handler(filters=mock_filter)
|
||||
assert not is_match(handler, make_message_update('/test'))
|
||||
assert not mock_filter.tested
|
||||
|
||||
class TestFilter(BaseFilter):
|
||||
def __init__(self):
|
||||
self.tested = False
|
||||
|
||||
def filter(self, message):
|
||||
self.tested = True
|
||||
|
||||
test_filter = TestFilter()
|
||||
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
|
||||
filters=test_filter)
|
||||
|
||||
prefixmessage.text = '/star'
|
||||
|
||||
check = handler.check_update(Update(0, message=prefixmessage))
|
||||
assert check is None or check is False
|
||||
|
||||
assert not test_filter.tested
|
||||
|
||||
def test_context(self, cdp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_context)
|
||||
def test_context(self, cdp, prefix_message_update):
|
||||
handler = self.make_default_handler(self.callback_context)
|
||||
cdp.add_handler(handler)
|
||||
assert self.response(cdp, prefix_message_update)
|
||||
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert self.test_flag
|
||||
def test_context_args(self, cdp, prefix_message_text):
|
||||
handler = self.make_default_handler(self.callback_context_args)
|
||||
self._test_context_args_or_regex(cdp, handler, prefix_message_text)
|
||||
|
||||
def test_context_args(self, cdp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
|
||||
self.callback_context_args)
|
||||
cdp.add_handler(handler)
|
||||
def test_context_regex(self, cdp, prefix_message_text):
|
||||
handler = self.make_default_handler(self.callback_context_regex1,
|
||||
filters=Filters.regex('one two'))
|
||||
self._test_context_args_or_regex(cdp, handler, prefix_message_text)
|
||||
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert not self.test_flag
|
||||
|
||||
prefixmessage.text += ' one two'
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
def test_context_regex(self, cdp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
|
||||
self.callback_context_regex1, Filters.regex('one two'))
|
||||
cdp.add_handler(handler)
|
||||
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert not self.test_flag
|
||||
|
||||
prefixmessage.text += ' one two'
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
def test_context_multiple_regex(self, cdp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
|
||||
self.callback_context_regex2,
|
||||
Filters.regex('one') & Filters.regex('two'))
|
||||
cdp.add_handler(handler)
|
||||
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert not self.test_flag
|
||||
|
||||
prefixmessage.text += ' one two'
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert self.test_flag
|
||||
def test_context_multiple_regex(self, cdp, prefix_message_text):
|
||||
handler = self.make_default_handler(self.callback_context_regex2,
|
||||
filters=Filters.regex('one') & Filters.regex(
|
||||
'two'))
|
||||
self._test_context_args_or_regex(cdp, handler, prefix_message_text)
|
||||
|
||||
@@ -43,6 +43,10 @@ class TestConversationHandler(object):
|
||||
# and then we can start coding!
|
||||
END, THIRSTY, BREWING, DRINKING, CODING = range(-1, 4)
|
||||
|
||||
# Drinking state definitions (nested)
|
||||
# At first we're holding the cup. Then we sip coffee, and last we swallow it
|
||||
HOLDING, SIPPING, SWALLOWING, REPLENISHING, STOPPING = map(chr, range(ord('a'), ord('f')))
|
||||
|
||||
current_state, entry_points, states, fallbacks = None, None, None, None
|
||||
group = Chat(0, Chat.GROUP)
|
||||
second_group = Chat(1, Chat.GROUP)
|
||||
@@ -69,6 +73,43 @@ class TestConversationHandler(object):
|
||||
self.fallbacks = [CommandHandler('eat', self.start)]
|
||||
self.is_timeout = False
|
||||
|
||||
# for nesting tests
|
||||
self.nested_states = {
|
||||
self.THIRSTY: [CommandHandler('brew', self.brew), CommandHandler('wait', self.start)],
|
||||
self.BREWING: [CommandHandler('pourCoffee', self.drink)],
|
||||
self.CODING: [
|
||||
CommandHandler('keepCoding', self.code),
|
||||
CommandHandler('gettingThirsty', self.start),
|
||||
CommandHandler('drinkMore', self.drink)
|
||||
],
|
||||
}
|
||||
self.drinking_entry_points = [CommandHandler('hold', self.hold)]
|
||||
self.drinking_states = {
|
||||
self.HOLDING: [CommandHandler('sip', self.sip)],
|
||||
self.SIPPING: [CommandHandler('swallow', self.swallow)],
|
||||
self.SWALLOWING: [CommandHandler('hold', self.hold)]
|
||||
}
|
||||
self.drinking_fallbacks = [CommandHandler('replenish', self.replenish),
|
||||
CommandHandler('stop', self.stop),
|
||||
CommandHandler('end', self.end),
|
||||
CommandHandler('startCoding', self.code),
|
||||
CommandHandler('drinkMore', self.drink)]
|
||||
self.drinking_entry_points.extend(self.drinking_fallbacks)
|
||||
|
||||
# Map nested states to parent states:
|
||||
self.drinking_map_to_parent = {
|
||||
# Option 1 - Map a fictional internal state to an external parent state
|
||||
self.REPLENISHING: self.BREWING,
|
||||
# Option 2 - Map a fictional internal state to the END state on the parent
|
||||
self.STOPPING: self.END,
|
||||
# Option 3 - Map the internal END state to an external parent state
|
||||
self.END: self.CODING,
|
||||
# Option 4 - Map an external state to the same external parent state
|
||||
self.CODING: self.CODING,
|
||||
# Option 5 - Map an external state to the internal entry point
|
||||
self.DRINKING: self.DRINKING
|
||||
}
|
||||
|
||||
# State handlers
|
||||
def _set_state(self, update, state):
|
||||
self.current_state[update.message.from_user.id] = state
|
||||
@@ -103,6 +144,23 @@ class TestConversationHandler(object):
|
||||
def passout2(self, bot, update):
|
||||
self.is_timeout = True
|
||||
|
||||
# Drinking actions (nested)
|
||||
|
||||
def hold(self, bot, update):
|
||||
return self._set_state(update, self.HOLDING)
|
||||
|
||||
def sip(self, bot, update):
|
||||
return self._set_state(update, self.SIPPING)
|
||||
|
||||
def swallow(self, bot, update):
|
||||
return self._set_state(update, self.SWALLOWING)
|
||||
|
||||
def replenish(self, bot, update):
|
||||
return self._set_state(update, self.REPLENISHING)
|
||||
|
||||
def stop(self, bot, update):
|
||||
return self._set_state(update, self.STOPPING)
|
||||
|
||||
# Tests
|
||||
def test_per_all_false(self):
|
||||
with pytest.raises(ValueError, match="can't all be 'False'"):
|
||||
@@ -609,3 +667,108 @@ class TestConversationHandler(object):
|
||||
"If 'per_chat=True', 'InlineQueryHandler' can not be used,"
|
||||
" since inline queries have no chat context."
|
||||
)
|
||||
|
||||
def test_nested_conversation_handler(self, dp, bot, user1, user2):
|
||||
self.nested_states[self.DRINKING] = [ConversationHandler(
|
||||
entry_points=self.drinking_entry_points,
|
||||
states=self.drinking_states,
|
||||
fallbacks=self.drinking_fallbacks,
|
||||
map_to_parent=self.drinking_map_to_parent)]
|
||||
handler = ConversationHandler(entry_points=self.entry_points,
|
||||
states=self.nested_states,
|
||||
fallbacks=self.fallbacks)
|
||||
dp.add_handler(handler)
|
||||
|
||||
# User one, starts the state machine.
|
||||
message = Message(0, user1, None, self.group, text='/start', bot=bot,
|
||||
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
|
||||
offset=0, length=len('/start'))])
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.THIRSTY
|
||||
|
||||
# The user is thirsty and wants to brew coffee.
|
||||
message.text = '/brew'
|
||||
message.entities[0].length = len('/brew')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.BREWING
|
||||
|
||||
# Lets pour some coffee.
|
||||
message.text = '/pourCoffee'
|
||||
message.entities[0].length = len('/pourCoffee')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.DRINKING
|
||||
|
||||
# The user is holding the cup
|
||||
message.text = '/hold'
|
||||
message.entities[0].length = len('/hold')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.HOLDING
|
||||
|
||||
# The user is sipping coffee
|
||||
message.text = '/sip'
|
||||
message.entities[0].length = len('/sip')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.SIPPING
|
||||
|
||||
# The user is swallowing
|
||||
message.text = '/swallow'
|
||||
message.entities[0].length = len('/swallow')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.SWALLOWING
|
||||
|
||||
# The user is holding the cup again
|
||||
message.text = '/hold'
|
||||
message.entities[0].length = len('/hold')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.HOLDING
|
||||
|
||||
# The user wants to replenish the coffee supply
|
||||
message.text = '/replenish'
|
||||
message.entities[0].length = len('/replenish')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.REPLENISHING
|
||||
assert handler.conversations[(0, user1.id)] == self.BREWING
|
||||
|
||||
# The user wants to drink their coffee again
|
||||
message.text = '/pourCoffee'
|
||||
message.entities[0].length = len('/pourCoffee')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.DRINKING
|
||||
|
||||
# The user is now ready to start coding
|
||||
message.text = '/startCoding'
|
||||
message.entities[0].length = len('/startCoding')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.CODING
|
||||
|
||||
# The user decides it's time to drink again
|
||||
message.text = '/drinkMore'
|
||||
message.entities[0].length = len('/drinkMore')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.DRINKING
|
||||
|
||||
# The user is holding their cup
|
||||
message.text = '/hold'
|
||||
message.entities[0].length = len('/hold')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.HOLDING
|
||||
|
||||
# The user wants to end with the drinking and go back to coding
|
||||
message.text = '/end'
|
||||
message.entities[0].length = len('/end')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.END
|
||||
assert handler.conversations[(0, user1.id)] == self.CODING
|
||||
|
||||
# The user wants to drink once more
|
||||
message.text = '/drinkMore'
|
||||
message.entities[0].length = len('/drinkMore')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.DRINKING
|
||||
|
||||
# The user wants to stop altogether
|
||||
message.text = '/stop'
|
||||
message.entities[0].length = len('/stop')
|
||||
dp.process_update(Update(update_id=0, message=message))
|
||||
assert self.current_state[user1.id] == self.STOPPING
|
||||
assert handler.conversations.get((0, user1.id)) is None
|
||||
|
||||
Reference in New Issue
Block a user