botserver: Allow importing custom bot modules.

We can now specify path to a bot's python file as the ini section
header in the botserver's config file. For example:

[~/Documents/helloworld.py]
email=a@b.com
key=XXXX
site=https://b.com
token=XXXX
This commit is contained in:
Rohitt Vashishtha 2019-08-28 04:49:29 +05:30 committed by Tim Abbott
parent 804501610b
commit 74d902d14f
2 changed files with 47 additions and 2 deletions

View file

@ -4,6 +4,8 @@ from typing import Any, Dict
import unittest import unittest
from .server_test_lib import BotServerTestCase from .server_test_lib import BotServerTestCase
import json import json
from importlib import import_module
from types import ModuleType
from zulip_botserver import server from zulip_botserver import server
from zulip_botserver.input_parameters import parse_args from zulip_botserver.input_parameters import parse_args
@ -177,5 +179,35 @@ class BotServerTests(BotServerTestCase):
} }
assert json.dumps(bot_conf2, sort_keys=True) == json.dumps(expected_config2, sort_keys=True) assert json.dumps(bot_conf2, sort_keys=True) == json.dumps(expected_config2, sort_keys=True)
def test_load_lib_modules(self) -> None:
# This testcase requires hardcoded paths, which here is a good thing so if we ever
# restructure zulip_bots, this test would fail and we would also update Botserver
# at the same time.
helloworld = import_module('zulip_bots.bots.{bot}.{bot}'.format(bot='helloworld'))
root_dir = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../'))
# load valid module name
module = server.load_lib_modules(['helloworld'])['helloworld']
assert module == helloworld
# load valid file path
path = os.path.join(root_dir, 'zulip_bots/zulip_bots/bots/{bot}/{bot}.py'.format(bot='helloworld'))
module = server.load_lib_modules([path])[path]
assert module.__name__ == 'custom_bot_module'
assert module.__file__ == path
assert isinstance(module, ModuleType)
# load invalid module name
with self.assertRaisesRegexp(SystemExit, # type: ignore
'Error: Bot "botserver-test-case-random-bot" doesn\'t exist. '
'Please make sure you have set up the botserverrc file correctly.'):
module = server.load_lib_modules(['botserver-test-case-random-bot'])['botserver-test-case-random-bot']
# load invalid file path
with self.assertRaisesRegexp(SystemExit, # type: ignore
'Error: Bot "{}/zulip_bots/zulip_bots/bots/helloworld.py" doesn\'t exist. '
'Please make sure you have set up the botserverrc file correctly.'.format(root_dir)):
path = os.path.join(root_dir, 'zulip_bots/zulip_bots/bots/{bot}.py'.format(bot='helloworld'))
module = server.load_lib_modules([path])[path]
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View file

@ -5,11 +5,13 @@ import logging
import json import json
import os import os
import sys import sys
import importlib.util
from configparser import MissingSectionHeaderError, NoOptionError from configparser import MissingSectionHeaderError, NoOptionError
from flask import Flask, request from flask import Flask, request
from importlib import import_module from importlib import import_module
from typing import Any, Dict, Union, List, Optional from typing import Any, Dict, Union, List, Optional
from types import ModuleType
from werkzeug.exceptions import BadRequest, Unauthorized from werkzeug.exceptions import BadRequest, Unauthorized
from zulip import Client from zulip import Client
@ -68,13 +70,24 @@ def parse_config_file(config_file_path: str) -> configparser.ConfigParser:
parser.read(config_file_path) parser.read(config_file_path)
return parser return parser
def load_module_from_file(file_path: str) -> ModuleType:
# Wrapper around importutil; see https://stackoverflow.com/a/67692/3909240.
spec = importlib.util.spec_from_file_location("custom_bot_module", file_path)
lib_module = importlib.util.module_from_spec(spec)
assert spec is not None
assert spec.loader is not None
spec.loader.exec_module(lib_module)
return lib_module
def load_lib_modules(available_bots: List[str]) -> Dict[str, Any]: def load_lib_modules(available_bots: List[str]) -> Dict[str, Any]:
bots_lib_module = {} bots_lib_module = {}
for bot in available_bots: for bot in available_bots:
try: try:
module_name = 'zulip_bots.bots.{bot}.{bot}'.format(bot=bot) if bot.endswith('.py') and os.path.isfile(bot):
lib_module = import_module(module_name) lib_module = load_module_from_file(bot)
else:
module_name = 'zulip_bots.bots.{bot}.{bot}'.format(bot=bot)
lib_module = import_module(module_name)
bots_lib_module[bot] = lib_module bots_lib_module[bot] = lib_module
except ImportError: except ImportError:
error_message = ("Error: Bot \"{}\" doesn't exist. Please make sure " error_message = ("Error: Bot \"{}\" doesn't exist. Please make sure "