diff --git a/zulip_bots/zulip_bots/run.py b/zulip_bots/zulip_bots/run.py index 6c8431d7d..6a1ec2ec8 100755 --- a/zulip_bots/zulip_bots/run.py +++ b/zulip_bots/zulip_bots/run.py @@ -4,6 +4,7 @@ import logging import os import sys +from pathlib import Path from typing import Optional from zulip_bots import finder @@ -111,6 +112,28 @@ def exit_gracefully_if_bot_config_file_does_not_exist(bot_config_file: Optional[ sys.exit(1) +def print_bot_not_found_error(bot: str, registry: bool) -> None: + if registry: + print( + f'ERROR: Could not find a bot named "{bot}" in the zulip_bots registry.\n' + "Make sure the package that provides it is installed. Exiting now." + ) + return + + bundled_bot_path = Path(finder.current_dir, "bots", bot, bot + ".py") + print( + f"""ERROR: Could not load bot module for "{bot}". Looked for: + - a file at {os.path.abspath(bot)} + - {bundled_bot_path} (one of the bots included with zulip_bots) + - a Python module named "{bot}" that can be imported from PYTHONPATH + +If you wrote this bot yourself, try passing the path to its .py file, e.g.: + zulip-run-bot path/to/{Path(bot).stem}.py --config-file ~/zuliprc + +Exiting now.""" + ) + + def main() -> None: args = parse_args() @@ -156,11 +179,11 @@ def main() -> None: bot_name = lib_module.__name__ bot_source = "named module" if args.provision: - print("ERROR: Could not load bot's module for '{}'. Exiting now.") + print(f"ERROR: Could not load bot's module for '{args.bot}'. Exiting now.") sys.exit(1) if lib_module is None: - print("ERROR: Could not load bot module. Exiting now.") + print_bot_not_found_error(args.bot, args.registry) sys.exit(1) if not args.quiet: diff --git a/zulip_bots/zulip_bots/tests/test_run.py b/zulip_bots/zulip_bots/tests/test_run.py index ead3f88ab..de02c32ba 100644 --- a/zulip_bots/zulip_bots/tests/test_run.py +++ b/zulip_bots/zulip_bots/tests/test_run.py @@ -1,5 +1,7 @@ +import io import os import sys +from contextlib import redirect_stdout from pathlib import Path from typing import Optional from unittest import TestCase, mock @@ -127,6 +129,50 @@ def test_run_bot_by_module_name(self, mock_os_path_isfile: mock.Mock) -> None: zulip_bots.run.main() mock_import_module.assert_called_once_with(bot_module_name) + @patch("os.path.isfile", return_value=False) + def test_provision_with_bot_module_name_shows_bot_name( + self, mock_os_path_isfile: mock.Mock + ) -> None: + mock_bot_module = mock.Mock() + mock_bot_module.__name__ = "bot.module.name" + output = io.StringIO() + with patch("sys.argv", ["zulip-run-bot", "bot.module.name", "--provision"]): + with patch("importlib.import_module", return_value=mock_bot_module): + with redirect_stdout(output), self.assertRaises(SystemExit): + zulip_bots.run.main() + + self.assertIn("'bot.module.name'", output.getvalue()) + self.assertNotIn("{}", output.getvalue()) + + @patch("os.path.isfile", return_value=False) + def test_bot_not_found_error_suggests_passing_path( + self, mock_os_path_isfile: mock.Mock + ) -> None: + output = io.StringIO() + with patch("sys.argv", ["zulip-run-bot", "mybot", "--config-file", "/path/to/config"]): + with patch("zulip_bots.finder.import_module_by_name", return_value=None): + with redirect_stdout(output), self.assertRaises(SystemExit): + zulip_bots.run.main() + + error = output.getvalue() + self.assertIn('Could not load bot module for "mybot"', error) + self.assertIn(str(Path("bots", "mybot", "mybot.py")), error) + self.assertIn("PYTHONPATH", error) + self.assertIn("zulip-run-bot path/to/mybot.py", error) + + def test_bot_not_found_in_registry_error(self) -> None: + output = io.StringIO() + with patch( + "sys.argv", ["zulip-run-bot", "mybot", "--config-file", "/path/to/config", "-r"] + ): + with patch("zulip_bots.finder.metadata.entry_points", return_value=()): + with redirect_stdout(output), self.assertRaises(SystemExit): + zulip_bots.run.main() + + self.assertIn( + 'Could not find a bot named "mybot" in the zulip_bots registry', output.getvalue() + ) + class TestBotLib(TestCase): def test_extract_query_without_mention(self) -> None: