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:
parent
804501610b
commit
74d902d14f
|
@ -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()
|
||||||
|
|
|
@ -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 "
|
||||||
|
|
Loading…
Reference in a new issue