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:
parent
536ba1843a
commit
80e4ef9f72
|
@ -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.")
|
||||
|
|
|
@ -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.
|
||||
```
|
||||
|
|
|
@ -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,
|
||||
|
|
80
zulip_bots/zulip_bots/simple_lib.py
Normal file
80
zulip_bots/zulip_bots/simple_lib.py
Normal 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))
|
73
zulip_bots/zulip_bots/terminal.py
Normal file
73
zulip_bots/zulip_bots/terminal.py
Normal 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()
|
|
@ -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()
|
Loading…
Reference in a new issue