Create terminal.py to run bots in the terminal.

This program replaces zulip_bot_output.py, which had
gotten a little out of date.

It should be able to simulate a terminal conversation for
all of our bots, including those that use "advanced" features:

    third party config files: tested with giphy
    message updates: tested with incrementor
    storage: tested with virtual_fs and others
This commit is contained in:
Steve Howell 2017-11-28 08:44:39 -08:00 committed by showell
parent 536ba1843a
commit 80e4ef9f72
6 changed files with 159 additions and 111 deletions

View file

@ -25,6 +25,9 @@ exclude = [
# fully annotate their bots.
"zulip_bots/zulip_bots/bots",
"zulip_bots/zulip_bots/bots_unmaintained",
# Excluded out of laziness:
"zulip_bots/zulip_bots/terminal.py",
"zulip_bots/zulip_bots/simple_lib.py",
]
parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.")

View file

@ -17,9 +17,10 @@ zulip_bots # This directory
│ ├───lib.py # Backbone of run.py
│ ├───provision.py # Creates a development environment.
│ ├───run.py # Used to run bots.
│ ├───simple_lib.py # Used for terminal testing.
│ ├───test_lib.py # Backbone for bot unit tests.
│ ├───test_run.py # Unit tests for run.py
│ └───zulip_bot_output.py # Used to test bots in the command line.
│ └───terminal.py # Used to test bots in the command line.
├───generate_manifest.py # Helper-script for packaging.
└───setup.py # Script for packaging.
```

View file

@ -36,7 +36,7 @@ package_info = dict(
entry_points={
'console_scripts': [
'zulip-run-bot=zulip_bots.run:main',
'zulip-bot-output=zulip_bots.zulip_bot_output:main'
'zulip-terminal=zulip_bots.terminal:main'
],
},
include_package_data=True,

View file

@ -0,0 +1,80 @@
import configparser
class SimpleStorage:
def __init__(self):
self.data = dict()
def contains(self, key):
return (key in self.data)
def put(self, key, value):
self.data[key] = value
def get(self, key):
return self.data[key]
class SimpleMessageServer:
# This class is needed for the incrementor bot, which
# actually updates messages!
def __init__(self):
self.message_id = 0
self.messages = dict()
def send(self, message):
self.message_id += 1
message['id'] = self.message_id
self.messages[self.message_id] = message
return message
def update(self, message):
self.messages[message['message_id']] = message
class TerminalBotHandler:
def __init__(self, bot_config_file):
self.bot_config_file = bot_config_file
self.storage = SimpleStorage()
self.message_server = SimpleMessageServer()
def send_message(self, message):
if message['type'] == 'stream':
print('''
stream: {} topic: {}
{}
'''.format(message['to'], message['subject'], message['content']))
else:
print('''
PM response:
{}
'''.format(message['content']))
return self.message_server.send(message)
def send_reply(self, message, response):
print('''
reply:
{}
'''.format(response))
response_message = dict(
content=response
)
return self.message_server.send(response_message)
def update_message(self, message):
self.message_server.update(message)
print('''
update to message #{}:
{}
'''.format(message['message_id'], message['content']))
def get_config_info(self, bot_name, optional=False):
if self.bot_config_file is None:
if optional:
return dict()
else:
print('Please supply --bot-config-file argument.')
sys.exit(1)
config = configparser.ConfigParser()
with open(self.bot_config_file) as conf:
config.readfp(conf) # type: ignore # readfp->read_file in python 3, so not in stubs
return dict(config.items(bot_name))

View file

@ -0,0 +1,73 @@
#!/usr/bin/env python
import os
import sys
import argparse
from zulip_bots.run import import_module_from_source
from zulip_bots.simple_lib import TerminalBotHandler
current_dir = os.path.dirname(os.path.abspath(__file__))
def parse_args():
description = '''
This tool allows you to test a bot using the terminal (and no Zulip server).
Examples: %(prog)s followup
'''
parser = argparse.ArgumentParser(description=description,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('bot',
action='store',
help='the name or path an existing bot to run')
parser.add_argument('--bot-config-file', '-b',
action='store',
help='optional third party config file (e.g. ~/giphy.conf)')
args = parser.parse_args()
return args
def main():
args = parse_args()
if os.path.isfile(args.bot):
bot_path = os.path.abspath(args.bot)
bot_name = os.path.splitext(os.path.basename(bot_path))[0]
else:
bot_path = os.path.abspath(os.path.join(current_dir, 'bots', args.bot, args.bot+'.py'))
bot_name = args.bot
bot_dir = os.path.dirname(bot_path)
try:
lib_module = import_module_from_source(bot_path, bot_name)
except IOError:
print("Could not find and import bot '{}'".format(bot_name))
sys.exit(1)
try:
message_handler = lib_module.handler_class()
except AttributeError:
print("This module does not appear to have a bot handler_class specified.")
sys.exit(1)
bot_handler = TerminalBotHandler(args.bot_config_file)
if hasattr(message_handler, 'initialize') and callable(message_handler.initialize):
message_handler.initialize(bot_handler)
sender_email = 'foo_sender@zulip.com'
while True:
content = input('Enter your message: ')
message = dict(
content=content,
sender_email=sender_email,
display_recipient=sender_email,
)
message_handler.handle_message(
message=message,
bot_handler=bot_handler,
)
if __name__ == '__main__':
main()

View file

@ -1,109 +0,0 @@
#!/usr/bin/env python
from __future__ import print_function
from __future__ import absolute_import
import os
import sys
import argparse
import zulip_bots
from six.moves import configparser
from typing import Any
from mock import MagicMock, patch
from zulip_bots.lib import StateHandler
from zulip_bots.lib import ExternalBotHandler
from zulip_bots.provision import provision_bot
from zulip_bots.run import import_module_from_source
current_dir = os.path.dirname(os.path.abspath(__file__))
def parse_args():
# type: () -> argparse.Namespace
description = (
"A tool to test a bot: given a provided message, provides the bot response.\n\n"
'Examples: %(prog)s xkcd 1\n'
' %(prog)s ./bot_folder/my_own_bot.py "test message"')
epilog = (
"The message need only be enclosed in quotes if empty or containing spaces.\n\n"
"(Internally, this program loads bot-related code from the library code and\n"
"then feeds the message provided in the command to the library code to handle.)")
parser = argparse.ArgumentParser(description=description,
epilog=epilog,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('bot',
action='store',
help='the name or path an existing bot to run')
parser.add_argument('message',
action='store',
help='the message content to send to the bot')
parser.add_argument('--force', '-f',
action='store_true',
help='try running bot even if dependencies install fails')
parser.add_argument('--provision', '-p',
action='store_true',
help='install dependencies for the bot')
args = parser.parse_args()
return args
def main():
# type: () -> None
args = parse_args()
if os.path.isfile(args.bot):
bot_path = os.path.abspath(args.bot)
bot_name = os.path.splitext(os.path.basename(bot_path))[0]
else:
bot_path = os.path.abspath(os.path.join(current_dir, 'bots', args.bot, args.bot+'.py'))
bot_name = args.bot
bot_dir = os.path.dirname(bot_path)
if args.provision:
provision_bot(os.path.dirname(bot_path), args.force)
try:
lib_module = import_module_from_source(bot_path, bot_name)
except IOError:
print("Could not find and import bot '{}'".format(bot_name))
sys.exit(1)
message = {'content': args.message, 'sender_email': 'foo_sender@zulip.com'}
try:
message_handler = lib_module.handler_class()
except AttributeError:
print("This module does not appear to have a bot handler_class specified.")
sys.exit(1)
with patch('zulip.Client') as mock_client:
mock_bot_handler = ExternalBotHandler(mock_client, bot_dir) # type: Any
mock_bot_handler.send_reply = MagicMock()
mock_bot_handler.send_message = MagicMock()
mock_bot_handler.update_message = MagicMock()
if hasattr(message_handler, 'initialize') and callable(message_handler.initialize):
message_handler.initialize(mock_bot_handler)
message_handler.handle_message(
message=message,
bot_handler=mock_bot_handler
)
print("On sending {} bot the message \"{}\"".format(bot_name, args.message))
# send_reply and send_message have slightly arguments; the
# following takes that into account.
# send_reply(original_message, response)
# send_message(response_message)
if mock_bot_handler.send_reply.called:
output_message = list(mock_bot_handler.send_reply.call_args)[0][1]
elif mock_bot_handler.send_message.called:
output_message = list(mock_bot_handler.send_message.call_args)[0][0]
elif mock_bot_handler.update_message.called:
output_message = list(mock_bot_handler.update_message.call_args)[0][0]['content']
print("the bot updates a message with the following text (in quotes):\n\"{}\"".format(output_message))
sys.exit()
else:
print("the bot sent no reply.")
sys.exit()
print("the bot gives the following output message (in quotes):\n\"{}\"".format(output_message))
if __name__ == '__main__':
main()