pw_rpc: Class for a 'help' command in a console
Change-Id: I48351dd80de515398cc29f60ac2788ce60f76489
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/33127
Reviewed-by: Alexei Frolov <frolv@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
diff --git a/pw_rpc/py/console_tools_test.py b/pw_rpc/py/console_tools_test.py
index 5da2aba..d75f67e 100644
--- a/pw_rpc/py/console_tools_test.py
+++ b/pw_rpc/py/console_tools_test.py
@@ -17,7 +17,7 @@
import unittest
from unittest import mock
-from pw_rpc.console_tools import Watchdog
+from pw_rpc.console_tools import CommandHelper, Watchdog
class TestWatchdog(unittest.TestCase):
@@ -75,5 +75,24 @@
self._expiration.assert_called()
+class TestCommandHelper(unittest.TestCase):
+ def setUp(self) -> None:
+ self._commands = {'command_a': 'A', 'command_B': 'B'}
+ self._helper = CommandHelper(self._commands, 'The header',
+ 'The footer')
+
+ def test_help_contents(self):
+ help_contents = self._helper.help()
+
+ self.assertTrue(help_contents.startswith('The header'))
+ self.assertIn('The footer', help_contents)
+
+ for cmd_name in self._commands:
+ self.assertIn(cmd_name, help_contents)
+
+ def test_repr_is_help(self):
+ self.assertEqual(repr(self._helper), self._helper.help())
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/pw_rpc/py/pw_rpc/console_tools.py b/pw_rpc/py/pw_rpc/console_tools.py
index cb43a6e..1b70238 100644
--- a/pw_rpc/py/pw_rpc/console_tools.py
+++ b/pw_rpc/py/pw_rpc/console_tools.py
@@ -13,8 +13,12 @@
# the License.
"""Utilities for building tools that interact with pw_rpc."""
+import inspect
+import textwrap
import threading
-from typing import Any, Callable
+from typing import Any, Callable, Dict, Iterable
+
+from pw_rpc.descriptors import Method
class Watchdog:
@@ -81,3 +85,53 @@
self._on_expiration()
self.start()
+
+
+_INDENT = ' '
+
+
+class CommandHelper:
+ """Used to implement a help command in an RPC console."""
+ @classmethod
+ def from_methods(cls,
+ methods: Iterable[Method],
+ header: str,
+ footer: str = '') -> 'CommandHelper':
+ return cls({m.full_name: m for m in methods}, header, footer)
+
+ def __init__(self, methods: Dict[str, Any], header: str, footer: str = ''):
+ self._methods = methods
+ self.header = header
+ self.footer = footer
+
+ def help(self, item: Any = None) -> str:
+ """Returns a help string with a command or all commands listed."""
+
+ if item is None:
+ all_rpcs = '\n'.join(self._methods)
+ return (f'{self.header}\n\n'
+ f'All commands:\n\n{textwrap.indent(all_rpcs, _INDENT)}'
+ f'\n\n{self.footer}'.strip())
+
+ # If item is a string, find commands matching that.
+ if isinstance(item, str):
+ matches = {n: m for n, m in self._methods.items() if item in n}
+ if not matches:
+ return f'No matches found for {item!r}'
+
+ if len(matches) == 1:
+ name, method = next(iter(matches.items()))
+ return f'{name}\n\n{inspect.getdoc(method)}'
+
+ return f'Multiple matches for {item!r}:\n\n' + textwrap.indent(
+ '\n'.join(matches), _INDENT)
+
+ return inspect.getdoc(item) or f'No documentation for {item!r}.'
+
+ def __call__(self, item: Any = None) -> None:
+ """Prints the help string."""
+ print(self.help(item))
+
+ def __repr__(self) -> str:
+ """Returns the help, so foo and foo() are equivalent in a console."""
+ return self.help()