Skip to content

Commit a232d12

Browse files
neiljpPIG208
authored andcommitted
bots: Find external packaged bots via 'zulip_bots.registry' entry_point.
Added dependency upon supporting small 'entrypoints' package. Add test case.
1 parent c602121 commit a232d12

File tree

3 files changed

+83
-25
lines changed

3 files changed

+83
-25
lines changed

zulip_bots/zulip_bots/finder.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import importlib
22
import importlib.abc
33
import importlib.util
4+
import importlib.metadata
45
import os
56
from pathlib import Path
67
from typing import Any, Optional, Tuple
@@ -25,6 +26,23 @@ def import_module_by_name(name: str) -> Any:
2526
return None
2627

2728

29+
class DuplicateRegisteredBotName(Exception):
30+
pass
31+
32+
33+
def import_module_from_zulip_bot_registry(name: str) -> Any:
34+
registered_bots = importlib.metadata.entry_points()["zulip_bots.registry"]
35+
matching_bots = [bot for bot in registered_bots if bot.name == name]
36+
37+
if len(matching_bots) == 1: # Unique matching entrypoint
38+
return matching_bots[0].load()
39+
40+
if len(matching_bots) > 1:
41+
raise DuplicateRegisteredBotName(name)
42+
43+
return None # no matches in registry
44+
45+
2846
def resolve_bot_path(name: str) -> Optional[Tuple[Path, str]]:
2947
if os.path.isfile(name):
3048
bot_path = Path(name)

zulip_bots/zulip_bots/run.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ def parse_args() -> argparse.Namespace:
4848
help="try running the bot even if dependencies install fails",
4949
)
5050

51+
parser.add_argument(
52+
"--registry",
53+
"-r",
54+
action="store_true",
55+
help="run the bot via zulipt_bot registry",
56+
)
57+
5158
parser.add_argument("--provision", action="store_true", help="install dependencies for the bot")
5259

5360
args = parser.parse_args()
@@ -109,36 +116,45 @@ def exit_gracefully_if_bot_config_file_does_not_exist(bot_config_file: Optional[
109116
def main() -> None:
110117
args = parse_args()
111118

112-
result = finder.resolve_bot_path(args.bot)
113-
if result:
114-
bot_path, bot_name = result
115-
sys.path.insert(0, os.path.dirname(bot_path))
116-
117-
if args.provision:
118-
provision_bot(os.path.dirname(bot_path), args.force)
119-
119+
if args.registry:
120120
try:
121-
lib_module = finder.import_module_from_source(bot_path.as_posix(), bot_name)
122-
except ImportError:
123-
req_path = os.path.join(os.path.dirname(bot_path), "requirements.txt")
124-
with open(req_path) as fp:
125-
deps_list = fp.read()
126-
127-
dep_err_msg = (
128-
"ERROR: The following dependencies for the {bot_name} bot are not installed:\n\n"
129-
"{deps_list}\n"
130-
"If you'd like us to install these dependencies, run:\n"
131-
" zulip-run-bot {bot_name} --provision"
132-
)
133-
print(dep_err_msg.format(bot_name=bot_name, deps_list=deps_list))
121+
lib_module = finder.import_module_from_zulip_bot_registry(args.bot)
122+
except finder.DuplicateRegisteredBotName:
123+
print("ERROR: Found duplicate entries for bot name in zulip bot registry. Exiting now.")
134124
sys.exit(1)
135-
else:
136-
lib_module = finder.import_module_by_name(args.bot)
137125
if lib_module:
138-
bot_name = lib_module.__name__
126+
bot_name = args.bot
127+
else:
128+
result = finder.resolve_bot_path(args.bot)
129+
if result:
130+
bot_path, bot_name = result
131+
sys.path.insert(0, os.path.dirname(bot_path))
132+
139133
if args.provision:
140-
print("ERROR: Could not load bot's module for '{}'. Exiting now.")
134+
provision_bot(os.path.dirname(bot_path), args.force)
135+
136+
try:
137+
lib_module = finder.import_module_from_source(bot_path.as_posix(), bot_name)
138+
except ImportError:
139+
req_path = os.path.join(os.path.dirname(bot_path), "requirements.txt")
140+
with open(req_path) as fp:
141+
deps_list = fp.read()
142+
143+
dep_err_msg = (
144+
"ERROR: The following dependencies for the {bot_name} bot are not installed:\n\n"
145+
"{deps_list}\n"
146+
"If you'd like us to install these dependencies, run:\n"
147+
" zulip-run-bot {bot_name} --provision"
148+
)
149+
print(dep_err_msg.format(bot_name=bot_name, deps_list=deps_list))
141150
sys.exit(1)
151+
else:
152+
lib_module = finder.import_module_by_name(args.bot)
153+
if lib_module:
154+
bot_name = lib_module.__name__
155+
if args.provision:
156+
print("ERROR: Could not load bot's module for '{}'. Exiting now.")
157+
sys.exit(1)
142158

143159
if lib_module is None:
144160
print("ERROR: Could not load bot module. Exiting now.")

zulip_bots/zulip_bots/tests/test_run.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#!/usr/bin/env python3
2+
import importlib
23
import os
34
import sys
45
import unittest
6+
from importlib.metadata import EntryPoint
57
from pathlib import Path
68
from typing import Optional
79
from unittest import TestCase, mock
@@ -15,6 +17,7 @@ class TestDefaultArguments(TestCase):
1517

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

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

54+
@patch("sys.argv", ["zulip-run-bot", "packaged_bot", "--config-file", "/foo/bar/baz.conf"])
55+
@patch("zulip_bots.run.run_message_handler_for_bot")
56+
def test_argument_parsing_with_zulip_bot_registry(
57+
self, mock_run_message_handler_for_bot: mock.Mock
58+
) -> None:
59+
with patch("zulip_bots.run.exit_gracefully_if_zulip_config_is_missing"):
60+
with patch("zulip_bots.finder.importlib.metadata.EntryPoint.load"):
61+
with patch(
62+
"zulip_bots.finder.importlib.metadata.entry_points",
63+
return_value={"zulip_bots.registry": [self.packaged_bot_entrypoint]},
64+
):
65+
zulip_bots.run.main()
66+
67+
mock_run_message_handler_for_bot.assert_called_with(
68+
bot_name="packaged_bot",
69+
config_file="/foo/bar/baz.conf",
70+
bot_config_file=None,
71+
lib_module=mock.ANY,
72+
quiet=False,
73+
)
74+
5175
def test_adding_bot_parent_dir_to_sys_path_when_bot_name_specified(self) -> None:
5276
bot_name = "helloworld" # existing bot's name
5377
expected_bot_dir_path = Path(

0 commit comments

Comments
 (0)