Skip to content

Commit

Permalink
bots: Find external packaged bots via 'zulip_bots.registry' entry_point.
Browse files Browse the repository at this point in the history
Added dependency upon supporting small 'entrypoints' package.

Add test case.
  • Loading branch information
neiljp authored and PIG208 committed Jul 21, 2021
1 parent c602121 commit a232d12
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 25 deletions.
18 changes: 18 additions & 0 deletions zulip_bots/zulip_bots/finder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import importlib
import importlib.abc
import importlib.util
import importlib.metadata
import os
from pathlib import Path
from typing import Any, Optional, Tuple
Expand All @@ -25,6 +26,23 @@ def import_module_by_name(name: str) -> Any:
return None


class DuplicateRegisteredBotName(Exception):
pass


def import_module_from_zulip_bot_registry(name: str) -> Any:
registered_bots = importlib.metadata.entry_points()["zulip_bots.registry"]
matching_bots = [bot for bot in registered_bots if bot.name == name]

if len(matching_bots) == 1: # Unique matching entrypoint
return matching_bots[0].load()

if len(matching_bots) > 1:
raise DuplicateRegisteredBotName(name)

return None # no matches in registry


def resolve_bot_path(name: str) -> Optional[Tuple[Path, str]]:
if os.path.isfile(name):
bot_path = Path(name)
Expand Down
66 changes: 41 additions & 25 deletions zulip_bots/zulip_bots/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ def parse_args() -> argparse.Namespace:
help="try running the bot even if dependencies install fails",
)

parser.add_argument(
"--registry",
"-r",
action="store_true",
help="run the bot via zulipt_bot registry",
)

parser.add_argument("--provision", action="store_true", help="install dependencies for the bot")

args = parser.parse_args()
Expand Down Expand Up @@ -109,36 +116,45 @@ def exit_gracefully_if_bot_config_file_does_not_exist(bot_config_file: Optional[
def main() -> None:
args = parse_args()

result = finder.resolve_bot_path(args.bot)
if result:
bot_path, bot_name = result
sys.path.insert(0, os.path.dirname(bot_path))

if args.provision:
provision_bot(os.path.dirname(bot_path), args.force)

if args.registry:
try:
lib_module = finder.import_module_from_source(bot_path.as_posix(), bot_name)
except ImportError:
req_path = os.path.join(os.path.dirname(bot_path), "requirements.txt")
with open(req_path) as fp:
deps_list = fp.read()

dep_err_msg = (
"ERROR: The following dependencies for the {bot_name} bot are not installed:\n\n"
"{deps_list}\n"
"If you'd like us to install these dependencies, run:\n"
" zulip-run-bot {bot_name} --provision"
)
print(dep_err_msg.format(bot_name=bot_name, deps_list=deps_list))
lib_module = finder.import_module_from_zulip_bot_registry(args.bot)
except finder.DuplicateRegisteredBotName:
print("ERROR: Found duplicate entries for bot name in zulip bot registry. Exiting now.")
sys.exit(1)
else:
lib_module = finder.import_module_by_name(args.bot)
if lib_module:
bot_name = lib_module.__name__
bot_name = args.bot
else:
result = finder.resolve_bot_path(args.bot)
if result:
bot_path, bot_name = result
sys.path.insert(0, os.path.dirname(bot_path))

if args.provision:
print("ERROR: Could not load bot's module for '{}'. Exiting now.")
provision_bot(os.path.dirname(bot_path), args.force)

try:
lib_module = finder.import_module_from_source(bot_path.as_posix(), bot_name)
except ImportError:
req_path = os.path.join(os.path.dirname(bot_path), "requirements.txt")
with open(req_path) as fp:
deps_list = fp.read()

dep_err_msg = (
"ERROR: The following dependencies for the {bot_name} bot are not installed:\n\n"
"{deps_list}\n"
"If you'd like us to install these dependencies, run:\n"
" zulip-run-bot {bot_name} --provision"
)
print(dep_err_msg.format(bot_name=bot_name, deps_list=deps_list))
sys.exit(1)
else:
lib_module = finder.import_module_by_name(args.bot)
if lib_module:
bot_name = lib_module.__name__
if args.provision:
print("ERROR: Could not load bot's module for '{}'. Exiting now.")
sys.exit(1)

if lib_module is None:
print("ERROR: Could not load bot module. Exiting now.")
Expand Down
24 changes: 24 additions & 0 deletions zulip_bots/zulip_bots/tests/test_run.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
import importlib
import os
import sys
import unittest
from importlib.metadata import EntryPoint
from pathlib import Path
from typing import Optional
from unittest import TestCase, mock
Expand All @@ -15,6 +17,7 @@ class TestDefaultArguments(TestCase):

our_dir = os.path.dirname(__file__)
path_to_bot = os.path.abspath(os.path.join(our_dir, "../bots/giphy/giphy.py"))
packaged_bot_entrypoint = EntryPoint("packaged_bot", "module_name", None)

@patch("sys.argv", ["zulip-run-bot", "giphy", "--config-file", "/foo/bar/baz.conf"])
@patch("zulip_bots.run.run_message_handler_for_bot")
Expand Down Expand Up @@ -48,6 +51,27 @@ def test_argument_parsing_with_bot_path(
quiet=False,
)

@patch("sys.argv", ["zulip-run-bot", "packaged_bot", "--config-file", "/foo/bar/baz.conf"])
@patch("zulip_bots.run.run_message_handler_for_bot")
def test_argument_parsing_with_zulip_bot_registry(
self, mock_run_message_handler_for_bot: mock.Mock
) -> None:
with patch("zulip_bots.run.exit_gracefully_if_zulip_config_is_missing"):
with patch("zulip_bots.finder.importlib.metadata.EntryPoint.load"):
with patch(
"zulip_bots.finder.importlib.metadata.entry_points",
return_value={"zulip_bots.registry": [self.packaged_bot_entrypoint]},
):
zulip_bots.run.main()

mock_run_message_handler_for_bot.assert_called_with(
bot_name="packaged_bot",
config_file="/foo/bar/baz.conf",
bot_config_file=None,
lib_module=mock.ANY,
quiet=False,
)

def test_adding_bot_parent_dir_to_sys_path_when_bot_name_specified(self) -> None:
bot_name = "helloworld" # existing bot's name
expected_bot_dir_path = Path(
Expand Down

0 comments on commit a232d12

Please sign in to comment.