diff --git a/rattail/commands/typer.py b/rattail/commands/typer.py index 7ccdbca8..8df23823 100644 --- a/rattail/commands/typer.py +++ b/rattail/commands/typer.py @@ -32,6 +32,8 @@ from typing import List, Optional import makefun import typer +from click import Context +from typer.core import TyperGroup from typing_extensions import Annotated from wuttjamaican.util import load_entry_points @@ -39,6 +41,28 @@ from rattail.config import make_config from rattail.progress import ConsoleProgress, SocketProgress +# nb. typer "by design" will not sort the commands listing, but we +# definitely want that (until someone says otherwise). +# nb. thanks to the following, for pointers +# https://stackoverflow.com/a/78351533 +# https://github.com/fastapi/typer/issues/428#issuecomment-1238866548 +class OrderCommands(TyperGroup): + """ + Custom base class for top-level Typer command. + + This exists only to ensure the commands listing is sorted when + displayed with ``--help`` param, since Typer "by design" will not + sort them. + + See also this `Typer doc + `_. + """ + + def list_commands(self, ctx: Context): + """ """ + return sorted(self.commands) + + def make_typer(**kwargs): """ Create a Typer command instance, per Rattail conventions. @@ -49,6 +73,7 @@ def make_typer(**kwargs): :returns: ``typer.Typer`` instance """ + kwargs.setdefault('cls', OrderCommands) kwargs.setdefault('callback', typer_callback) return typer.Typer(**kwargs) diff --git a/tests/commands/test_typer.py b/tests/commands/test_typer.py index 7c14d354..be9e28a1 100644 --- a/tests/commands/test_typer.py +++ b/tests/commands/test_typer.py @@ -8,6 +8,29 @@ from wuttjamaican.testing import FileConfigTestCase from rattail.commands import typer as mod +class TestOrderCommands(TestCase): + + def test_list_commands(self): + cmd = mod.make_typer() + ctx = MagicMock() + + @cmd.command() + def func_xyz(ctx): + pass + + @cmd.command() + def func_abc(ctx): + pass + + # TODO: ultimately i could not figure out how to inspect the + # typer cmd well enough to test anything, so had to just + # instantiate and mock the class under test + self.assertIs(cmd.info.cls, mod.OrderCommands) + inst = mod.OrderCommands() + with patch.object(inst, 'commands', new=['func_xyz', 'func_abc']): + self.assertEqual(inst.list_commands(ctx), ['func_abc', 'func_xyz']) + + class TestMakeCliConfig(FileConfigTestCase): def test_basic(self):