diff --git a/modules/commands/advert_command.py b/modules/commands/advert_command.py index 042c8a3..f01f548 100644 --- a/modules/commands/advert_command.py +++ b/modules/commands/advert_command.py @@ -37,7 +37,7 @@ class AdvertCommand(BaseCommand): super().__init__(bot) self.advert_enabled = self.get_config_value('Advert_Command', 'enabled', fallback=True, value_type='bool') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the advert command. Returns: diff --git a/modules/commands/airplanes_command.py b/modules/commands/airplanes_command.py index f89e559..5a6026a 100644 --- a/modules/commands/airplanes_command.py +++ b/modules/commands/airplanes_command.py @@ -103,7 +103,7 @@ class AirplanesCommand(BaseCommand): self.max_results = self.get_config_value('Airplanes_Command', 'max_results', fallback=3, value_type='int') self.url_timeout = self.get_config_value('Airplanes_Command', 'url_timeout', fallback=10, value_type='int') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for this command. Returns: diff --git a/modules/commands/alternatives/wx_international.py b/modules/commands/alternatives/wx_international.py index 860acb3..27c945d 100644 --- a/modules/commands/alternatives/wx_international.py +++ b/modules/commands/alternatives/wx_international.py @@ -141,7 +141,7 @@ class GlobalWxCommand(WeatherCommandMixin, BaseCommand): """ return load_open_meteo_model(self.bot.config, self.logger) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the command. Returns: diff --git a/modules/commands/aqi_command.py b/modules/commands/aqi_command.py index 10fc761..ae66a5c 100644 --- a/modules/commands/aqi_command.py +++ b/modules/commands/aqi_command.py @@ -135,7 +135,7 @@ class AqiCommand(BaseCommand): 'comet': "Comets have thin atmospheres of water vapor and dust. AQI: Variable, but you'd freeze in space anyway. ☄️" } - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: region = self.default_state or self.default_country return f"Usage: aqi - Get AQI for city/neighborhood in {region}, intl cities, coordinates, or help" diff --git a/modules/commands/base_command.py b/modules/commands/base_command.py index 9b8bd2e..93ccc5d 100644 --- a/modules/commands/base_command.py +++ b/modules/commands/base_command.py @@ -376,7 +376,7 @@ class BaseCommand(ABC): return False return True - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for this command. Returns: diff --git a/modules/commands/catfact_command.py b/modules/commands/catfact_command.py index 57d07f5..ca0c23f 100644 --- a/modules/commands/catfact_command.py +++ b/modules/commands/catfact_command.py @@ -117,7 +117,7 @@ class CatfactCommand(BaseCommand): """ return self.translated_or('commands.catfact.facts', self.cat_facts_fallback) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the catfact command. Returns: diff --git a/modules/commands/channelpause_command.py b/modules/commands/channelpause_command.py index c908353..edc1941 100644 --- a/modules/commands/channelpause_command.py +++ b/modules/commands/channelpause_command.py @@ -23,7 +23,7 @@ class ChannelPauseCommand(BaseCommand): def __init__(self, bot): super().__init__(bot) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: return ( "Controls whether the bot responds to public channel messages.\n" "DMs always work (including this command).\n" diff --git a/modules/commands/channels_command.py b/modules/commands/channels_command.py index b5c3b69..cce1cd7 100644 --- a/modules/commands/channels_command.py +++ b/modules/commands/channels_command.py @@ -63,7 +63,7 @@ class ChannelsCommand(BaseCommand): super().__init__(bot) self.channels_enabled = self.get_config_value('Channels_Command', 'enabled', fallback=True, value_type='bool') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: return self.translate('commands.channels.help') def matches_keyword(self, message: MeshMessage) -> bool: diff --git a/modules/commands/cmd_command.py b/modules/commands/cmd_command.py index 79b2a35..eea8a31 100644 --- a/modules/commands/cmd_command.py +++ b/modules/commands/cmd_command.py @@ -41,7 +41,7 @@ class CmdCommand(BaseCommand): 'Cmd_Command', 'cmd_reference_url', fallback='', value_type='str' ).strip() - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the cmd command. Returns: diff --git a/modules/commands/contact_command.py b/modules/commands/contact_command.py index f62683e..2a2f82d 100644 --- a/modules/commands/contact_command.py +++ b/modules/commands/contact_command.py @@ -39,7 +39,7 @@ class ContactCommand(BaseCommand): super().__init__(bot) self.enabled = self.get_config_value('Contact_Command', 'enabled', fallback=True, value_type='bool') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the contact command. Returns: diff --git a/modules/commands/dadjoke_command.py b/modules/commands/dadjoke_command.py index 433563a..22b7cd7 100644 --- a/modules/commands/dadjoke_command.py +++ b/modules/commands/dadjoke_command.py @@ -66,7 +66,7 @@ class DadJokeCommand(BaseCommand): self.dadjoke_enabled = self.get_config_value('DadJoke_Command', 'dadjoke_enabled', fallback=True, value_type='bool') self.long_jokes = self.get_config_value('DadJoke_Command', 'long_jokes', fallback=False, value_type='bool') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the dadjoke command. Returns: diff --git a/modules/commands/dice_command.py b/modules/commands/dice_command.py index 04797be..9ddebf9 100644 --- a/modules/commands/dice_command.py +++ b/modules/commands/dice_command.py @@ -57,7 +57,7 @@ class DiceCommand(BaseCommand): super().__init__(bot) self.dice_enabled = self.get_config_value('Dice_Command', 'enabled', fallback=True, value_type='bool') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the dice command. Returns: diff --git a/modules/commands/feed_command.py b/modules/commands/feed_command.py index c3b0de7..aa2b47b 100644 --- a/modules/commands/feed_command.py +++ b/modules/commands/feed_command.py @@ -83,7 +83,7 @@ class FeedCommand(BaseCommand): else: return await self.send_response(message, self.get_help_text()) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for feed command""" return """Feed Command Usage: feed subscribe [name] diff --git a/modules/commands/greeter_command.py b/modules/commands/greeter_command.py index 2914d58..8770ca7 100644 --- a/modules/commands/greeter_command.py +++ b/modules/commands/greeter_command.py @@ -1514,7 +1514,7 @@ class GreeterCommand(BaseCommand): # Mark as greeted so we don't greet them later self.mark_as_greeted(key[0], key[1]) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the greeter command. Returns: diff --git a/modules/commands/hacker_command.py b/modules/commands/hacker_command.py index 346dc26..0c347d2 100644 --- a/modules/commands/hacker_command.py +++ b/modules/commands/hacker_command.py @@ -429,7 +429,7 @@ class HackerCommand(BaseCommand): if self.enabled is None: self.enabled = self.get_config_value('Hacker_Command', 'hacker_enabled', fallback=False, value_type='bool') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the hacker command. Returns: diff --git a/modules/commands/hello_command.py b/modules/commands/hello_command.py index cf3dd48..4756fda 100644 --- a/modules/commands/hello_command.py +++ b/modules/commands/hello_command.py @@ -285,7 +285,7 @@ class HelloCommand(BaseCommand): """ return self.translated_or('commands.hello.emoji_responses', self.emoji_responses_fallback, dict) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the hello command. Returns: diff --git a/modules/commands/help_command.py b/modules/commands/help_command.py index e4ec998..bcb084c 100644 --- a/modules/commands/help_command.py +++ b/modules/commands/help_command.py @@ -47,7 +47,7 @@ class HelpCommand(BaseCommand): super().__init__(bot) self.help_enabled = self.get_config_value('Help_Command', 'enabled', fallback=True, value_type='bool') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the help command. Returns: diff --git a/modules/commands/hfcond_command.py b/modules/commands/hfcond_command.py index 0e0fb4f..cfe7ff6 100644 --- a/modules/commands/hfcond_command.py +++ b/modules/commands/hfcond_command.py @@ -61,7 +61,7 @@ class HfcondCommand(BaseCommand): error_msg = self.translate('commands.hfcond.error', error=str(e)) return await self.send_response(message, error_msg) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for this command. Returns: diff --git a/modules/commands/magic8_command.py b/modules/commands/magic8_command.py index 2c14ed4..837ae1e 100644 --- a/modules/commands/magic8_command.py +++ b/modules/commands/magic8_command.py @@ -46,7 +46,7 @@ class Magic8Command(BaseCommand): super().__init__(bot) self.magic8_enabled = self.get_config_value('Magic8_Command', 'enabled', fallback=True, value_type='bool') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the magic8 command. Returns: diff --git a/modules/commands/moon_command.py b/modules/commands/moon_command.py index 7675261..f8938b4 100644 --- a/modules/commands/moon_command.py +++ b/modules/commands/moon_command.py @@ -166,7 +166,7 @@ class MoonCommand(BaseCommand): # Fallback to original format if formatting fails return self.translate('commands.moon.fallback', info=moon_info) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for this command. Returns: diff --git a/modules/commands/multitest_command.py b/modules/commands/multitest_command.py index 02833ac..706ea93 100644 --- a/modules/commands/multitest_command.py +++ b/modules/commands/multitest_command.py @@ -530,7 +530,7 @@ class MultitestCommand(BaseCommand): """True when any condensed layout is enabled (flat or nested).""" return self.condense_paths_mode != "off" - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: return self.translate('commands.multitest.help', fallback="Listens for 6 seconds and collects all unique paths from incoming messages") def extract_path_from_rf_data(self, rf_data: dict) -> Optional[str]: diff --git a/modules/commands/neighbors_command.py b/modules/commands/neighbors_command.py index 0817c45..3f64601 100644 --- a/modules/commands/neighbors_command.py +++ b/modules/commands/neighbors_command.py @@ -60,7 +60,7 @@ class NeighborsCommand(BaseCommand): # where the scheduler's own trigger is also visible. self._cycle_task: Optional[asyncio.Task] = None - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the neighbors command. Returns: diff --git a/modules/commands/path_command.py b/modules/commands/path_command.py index 965d8f6..fdacc67 100644 --- a/modules/commands/path_command.py +++ b/modules/commands/path_command.py @@ -1097,6 +1097,6 @@ class PathCommand(BaseCommand): """Get help text for the path command""" return self.translate('commands.path.help') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the path command (used by help system)""" return self.get_help() diff --git a/modules/commands/ping_command.py b/modules/commands/ping_command.py index 26f8910..0a0bac5 100644 --- a/modules/commands/ping_command.py +++ b/modules/commands/ping_command.py @@ -39,7 +39,7 @@ class PingCommand(BaseCommand): super().__init__(bot) self.ping_enabled = self.get_config_value('Ping_Command', 'enabled', fallback=True, value_type='bool') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the ping command. Returns: diff --git a/modules/commands/prefix_command.py b/modules/commands/prefix_command.py index 1ffe8d1..3803118 100644 --- a/modules/commands/prefix_command.py +++ b/modules/commands/prefix_command.py @@ -157,7 +157,7 @@ class PrefixCommand(BaseCommand): self.prefix_best_location_radius_km = 50.0 self.prefix_best_do_not_suggest = [] - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the prefix command. Returns: diff --git a/modules/commands/reload_command.py b/modules/commands/reload_command.py index be4c555..f5b255a 100644 --- a/modules/commands/reload_command.py +++ b/modules/commands/reload_command.py @@ -29,7 +29,7 @@ class ReloadCommand(BaseCommand): """ super().__init__(bot) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the reload command. Returns: diff --git a/modules/commands/roll_command.py b/modules/commands/roll_command.py index 52b1042..0b98c01 100644 --- a/modules/commands/roll_command.py +++ b/modules/commands/roll_command.py @@ -45,7 +45,7 @@ class RollCommand(BaseCommand): super().__init__(bot) self.roll_enabled = self.get_config_value('Roll_Command', 'enabled', fallback=True, value_type='bool') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the roll command. Returns: diff --git a/modules/commands/satpass_command.py b/modules/commands/satpass_command.py index d44ecfa..2f2c206 100644 --- a/modules/commands/satpass_command.py +++ b/modules/commands/satpass_command.py @@ -103,7 +103,7 @@ class SatpassCommand(BaseCommand): await self.send_response(message, error_msg) return False - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for this command. Returns: diff --git a/modules/commands/schedule_command.py b/modules/commands/schedule_command.py index 124b537..465470d 100644 --- a/modules/commands/schedule_command.py +++ b/modules/commands/schedule_command.py @@ -56,7 +56,7 @@ class ScheduleCommand(BaseCommand): return False return super().can_execute(message) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: return "schedule [list] — show scheduled messages and advert interval" async def execute(self, message: MeshMessage) -> bool: diff --git a/modules/commands/solar_command.py b/modules/commands/solar_command.py index 78f11c9..ef65a2e 100644 --- a/modules/commands/solar_command.py +++ b/modules/commands/solar_command.py @@ -67,7 +67,7 @@ class SolarCommand(BaseCommand): await self.send_response(message, error_msg) return False - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for this command. Returns: diff --git a/modules/commands/solarforecast_command.py b/modules/commands/solarforecast_command.py index de37d37..d827df4 100644 --- a/modules/commands/solarforecast_command.py +++ b/modules/commands/solarforecast_command.py @@ -106,7 +106,7 @@ class SolarforecastCommand(BaseCommand): # Get database manager for geocoding cache self.db_manager = bot.db_manager - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: return self.translate('commands.solarforecast.usage') def _translate_day_abbreviation(self, day_abbr: str) -> str: diff --git a/modules/commands/sports_command.py b/modules/commands/sports_command.py index c132afe..16e7d7e 100644 --- a/modules/commands/sports_command.py +++ b/modules/commands/sports_command.py @@ -141,7 +141,7 @@ class SportsCommand(BaseCommand): return self._cleaned_content_matches(message, _matches) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: return self.translate('commands.sports.help') diff --git a/modules/commands/stats_command.py b/modules/commands/stats_command.py index fded731..18e3118 100644 --- a/modules/commands/stats_command.py +++ b/modules/commands/stats_command.py @@ -342,7 +342,7 @@ class StatsCommand(BaseCommand): # If it's already a single node ID or short path, return as-is return path - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the stats command. Returns: diff --git a/modules/commands/sun_command.py b/modules/commands/sun_command.py index 75e24ef..a3703df 100644 --- a/modules/commands/sun_command.py +++ b/modules/commands/sun_command.py @@ -62,7 +62,7 @@ class SunCommand(BaseCommand): error_msg = self.translate('commands.sun.error', error=str(e)) return await self.send_response(message, error_msg) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for this command. Returns: diff --git a/modules/commands/test_command.py b/modules/commands/test_command.py index d72a112..d639a9a 100644 --- a/modules/commands/test_command.py +++ b/modules/commands/test_command.py @@ -86,7 +86,7 @@ class TestCommand(BaseCommand): except Exception as e: self.logger.warning(f"Error reading bot location from config: {e}") - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the command. Returns: diff --git a/modules/commands/trace_command.py b/modules/commands/trace_command.py index 2b73168..e98eeb2 100644 --- a/modules/commands/trace_command.py +++ b/modules/commands/trace_command.py @@ -82,7 +82,7 @@ class TraceCommand(BaseCommand): output_fmt = (self.bot.config.get("Trace_Command", "output_format", fallback="inline") or "inline").strip().lower() self.output_format = output_fmt if output_fmt in ("inline", "vertical") else "inline" - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: return ( "trace [path] — run trace along path (return may not be heard). No path = round-trip like tracer. " "tracer [path] — round-trip so bot hears return. Path: comma-separated hex nodes " diff --git a/modules/commands/version_command.py b/modules/commands/version_command.py index b9bbf1b..4ba36fb 100644 --- a/modules/commands/version_command.py +++ b/modules/commands/version_command.py @@ -37,7 +37,7 @@ class VersionCommand(BaseCommand): value_type="bool", ) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: return self.description async def execute(self, message: MeshMessage) -> bool: diff --git a/modules/commands/webviewer_command.py b/modules/commands/webviewer_command.py index 04be678..33239e4 100644 --- a/modules/commands/webviewer_command.py +++ b/modules/commands/webviewer_command.py @@ -30,7 +30,7 @@ class WebViewerCommand(BaseCommand): super().__init__(bot) self.webviewer_enabled = self.get_config_value('WebViewer_Command', 'enabled', fallback=True, value_type='bool') - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text for the webviewer command. Returns: diff --git a/modules/commands/worldcup_command.py b/modules/commands/worldcup_command.py index bb9e3cf..06c4a8e 100644 --- a/modules/commands/worldcup_command.py +++ b/modules/commands/worldcup_command.py @@ -81,7 +81,7 @@ class WorldCupCommand(BaseCommand): return self._cleaned_content_matches(message, _matches) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: return self.translate("commands.worldcup.help") # ------------------------------------------------------------------ helpers diff --git a/modules/commands/wx_command.py b/modules/commands/wx_command.py index 754c02d..3d7b3b5 100644 --- a/modules/commands/wx_command.py +++ b/modules/commands/wx_command.py @@ -366,7 +366,7 @@ class WxCommand(WeatherCommandMixin, BaseCommand): """Move blocking provider work off-loop without concurrent Session use.""" return await asyncio.to_thread(self._run_sync_provider, operation, *args, **kwargs) - def get_help_text(self) -> str: + def get_help_text(self, message: MeshMessage | None = None) -> str: """Get help text, delegating to international command if using Open-Meteo""" if self.delegate_command: return self.delegate_command.get_help_text() diff --git a/tests/unit/test_help_text_signature.py b/tests/unit/test_help_text_signature.py new file mode 100644 index 0000000..a9199b9 --- /dev/null +++ b/tests/unit/test_help_text_signature.py @@ -0,0 +1,35 @@ +"""Every bundled command takes ``get_help_text(message=None)``. + +help and command_manager still retry without the message when a command's +get_help_text takes none (local plugins may be written that way), but bundled +commands use the one signature. +""" + +import importlib +import inspect +import pkgutil + +import modules.commands +import modules.commands.alternatives +from modules.commands.base_command import BaseCommand + + +def _bundled_command_classes(): + for package in (modules.commands, modules.commands.alternatives): + for info in pkgutil.iter_modules(package.__path__): + module = importlib.import_module(f"{package.__name__}.{info.name}") + for _, cls in inspect.getmembers(module, inspect.isclass): + if issubclass(cls, BaseCommand) and cls.__module__ == module.__name__: + yield cls + + +def test_every_bundled_get_help_text_takes_an_optional_message(): + classes = list(_bundled_command_classes()) + assert len(classes) > 40 + wrong = [] + for cls in classes: + params = inspect.signature(cls.get_help_text).parameters + message = params.get("message") + if message is None or message.default is not None: + wrong.append(cls.__name__) + assert wrong == []