Compare commits

..

12 Commits

Author SHA1 Message Date
Noam Meltzer 98147fce32 Bump version to v12.2.0 2019-10-14 21:12:13 +03:00
Poolitzer e54e9f2347 moving flake config to one file (#1546) 2019-10-14 11:10:51 +03:00
Poolitzer 3545139dd7 adding to_dict test (#1544)
fixes #1541
2019-10-12 23:40:42 +03:00
Poolitzer d0c27e2d46 adding 3.8 to travis and appveyor (#1543)
Fixes #1542
2019-10-12 16:12:41 +03:00
Paolo Lammens 3318239cf6 tests: refactor `test_commandhandler.py` (#1408)
- Improved usage of fixtures
    - Replaced fixtures for directly callable factories where
    multiple mock objects were needed in the same test function
    - Extracted fixtures where possible (in place of literals or
    global constants)
  - Moved some fixtures to ``conftest.py`` to be used by other
  modules
  - Made a common base class for both ``TestCommandHandler`` and
  ``TestPrefixHandler``, extracting common methods, patterns and
  signatures
    - The extracted patterns in test methods have been named with
    leading ``_test``
  - Extracted other repeatedly used test utilities into functions
  (e.g. ``is_match``) and methods (e.g. ``make_default_handler``)
2019-10-12 16:11:09 +03:00
Bibo-Joshi aadb6df271 Nested ConversationHandlers (#1512)
Fixes #405
2019-10-11 22:59:36 +03:00
Noah Evans 2cc9aac7dc Fix Bot.to_dict to use proper first_name (#1525)
Fixes #1519
2019-10-11 22:37:28 +03:00
Trainer Jono 1d007b1b60 Fix typos in examples (#1537) 2019-10-11 21:10:21 +03:00
Noam Meltzer 3257148d13 travis.yaml: TEST_OFFICIAL removed from allowed_failures
it was originally added to allowed_failures until we completed API 4.4
support
2019-10-11 21:02:32 +03:00
Poolitzer 805a798b50 Fix CI failures due to non-backward compat attrs depndency (#1540) 2019-10-11 21:01:25 +03:00
Noam Meltzer e60a42010b Bump version to v12.1.1 2019-09-17 11:30:52 +03:00
Noam Meltzer ae88129f0f Revert accidental change to the git revision of the urllib3 submodule (#1517)
Fixes #1516
2019-09-17 11:25:54 +03:00
26 changed files with 1017 additions and 576 deletions
-3
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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>`_
+37
View File
@@ -2,6 +2,43 @@
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*
**Hot fix release**
Fixed regression in the vendored urllib3 (`#1517`_).
.. _`#1517`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1517
Version 12.1.0
================
*Released 2019-09-13*
+5 -1
View File
@@ -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
View File
@@ -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.0' # telegram.__version__
release = '12.2.0' # telegram.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
+3
View File
@@ -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.
+1 -1
View File
@@ -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'
+2 -2
View File
@@ -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!')
+2 -2
View File
@@ -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!')
+4 -4
View File
@@ -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

+362
View File
@@ -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()
+2 -2
View File
@@ -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!")
+2 -2
View File
@@ -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')
+1
View File
@@ -8,3 +8,4 @@ beautifulsoup4
pytest==4.2.0
pytest-timeout
wheel
attrs==19.1.0
+2 -1
View File
@@ -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
View File
@@ -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
+28 -2
View File
@@ -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:
+5 -8
View File
@@ -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
View File
@@ -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.0'
__version__ = '12.2.0'
+110 -3
View File
@@ -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)
+12
View File
@@ -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
View File
@@ -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)
+163
View File
@@ -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