diff --git a/examples/categories/__init__.py b/examples/categories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/categories/categories.py b/examples/categories/categories.py new file mode 100644 index 00000000..8a2f11f7 --- /dev/null +++ b/examples/categories/categories.py @@ -0,0 +1,83 @@ +""" +This module demonstrates the use of categories in Fire. +Categories seperate GROUPS and COMMANDS into logical groups. +The effect is that the CLI is more organized and easier to navigate, +but only visually, as the command structure is not modified. +""" + +import os +import fire + +class GroupDemoClass: + def __init__(self): + pass + + def test1(self): + print("test1") + + def test2(self): + print("test2") + + def test3(self): + print("test3") + +class CategoryDemo: + # Groups category is "uncategorized" -> will be displayed as "GROUPS" in the CLI + group1 = GroupDemoClass() + + # Groups category is "GroupA" + group2 = GroupDemoClass() + group2.__fire_category__ = "A" + group3 = GroupDemoClass() + group3.__fire_category__ = "A" + + # Groups category is "GroupB" + group4 = GroupDemoClass() + group4.__fire_category__ = "B" + group5 = GroupDemoClass() + group5.__fire_category__ = "B" + + def __init__(self): + self.greeting = "Hello, World!" + + @fire.helptext.CommandCategory("hello") + def greet(self): + print(self.greeting) + + @fire.helptext.CommandCategory("hello") + def greet2(self): + print(self.greeting) + + @fire.helptext.CommandCategory("bye") + def farewell(self): + print("Goodbye, World!") + + @fire.helptext.CommandCategory("bye") + def farewell2(self): + print("Goodbye, World!") + + @fire.helptext.CommandCategory("bye") + @classmethod + def farewell3(cls): + print("Goodbye, World!") + + def test3(self): + print("test3") + + def test4(self): + print("test4") + + @classmethod + def test5(cls): + print("test5") + + @classmethod + def test6(cls): + print("test6") + +def main(): + os.environ["PAGER"] = "cat" + fire.Fire(CategoryDemo()) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/fire/helptext.py b/fire/helptext.py index 318d6276..eda186d9 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -31,6 +31,7 @@ import collections import itertools +import functools from fire import completion from fire import custom_descriptions @@ -281,17 +282,61 @@ def _ArgsAndFlagsSections(info, spec, metadata): return args_and_flags_sections, notes_sections +def _GetActionGroupByName(actions_grouped_by_kind, name): + for group in actions_grouped_by_kind: + if group.name == name: + return group + return None + + +def _GetActionGroupBetweenNames(actions_grouped_by_kind, first_name, next_name): + group_list = [] + in_between = False + for group in actions_grouped_by_kind: + if group.name == first_name: + in_between = True + if group.name == next_name: + in_between = False + if in_between: + group_list.append(group) + return group_list + + +def CommandCategory(category): + def decorator(command): + # Preserve the original function's signature and metadata + @functools.wraps(command) + def wrapper(*args, **kwargs): + return command(*args, **kwargs) + wrapper.__fire_category__ = category + + # If the command is a classmethod, preserve that functionality + if isinstance(command, classmethod): + return classmethod(wrapper) + return wrapper + return decorator + + def _UsageDetailsSections(component, actions_grouped_by_kind): """The usage details sections of the help string.""" - groups, commands, values, indexes = actions_grouped_by_kind + + group_list = _GetActionGroupBetweenNames(actions_grouped_by_kind, "group", "command") + command_list = _GetActionGroupBetweenNames(actions_grouped_by_kind, "command", "value") + values = _GetActionGroupByName(actions_grouped_by_kind, "value") + indexes = _GetActionGroupByName(actions_grouped_by_kind, "index") sections = [] - if groups.members: - sections.append(_MakeUsageDetailsSection(groups)) - if commands.members: - sections.append(_MakeUsageDetailsSection(commands)) + for group in group_list: + if group.members: + sections.append(_MakeUsageDetailsSection(group)) + + for command in command_list: + if command.members: + sections.append(_MakeUsageDetailsSection(command)) + if values.members: sections.append(_ValuesUsageDetailsSection(component, values)) + if indexes.members: sections.append(('INDEXES', _NewChoicesSection('INDEX', indexes.names))) @@ -379,13 +424,29 @@ def _GetActionsGroupedByKind(component, verbose=False): values = ActionGroup(name='value', plural='values') indexes = ActionGroup(name='index', plural='indexes') + # Groups and commands are grouped by category, other action groups are not + # A category is a dynamic action group that is created on the fly + # At the end we merge dynamic action groups with the default action groups + groups_by_category = {} + commands_by_category = {} + members = completion.VisibleMembers(component, verbose=verbose) for member_name, member in members: member_name = str(member_name) if value_types.IsGroup(member): - groups.Add(name=member_name, member=member) + if hasattr(member, '__fire_category__'): + if member.__fire_category__ not in groups_by_category: + groups_by_category[member.__fire_category__] = ActionGroup(name=member.__fire_category__, plural=member.__fire_category__) + groups_by_category[member.__fire_category__].Add(name=member_name, member=member) + else: + groups.Add(name=member_name, member=member) if value_types.IsCommand(member): - commands.Add(name=member_name, member=member) + if hasattr(member, '__fire_category__'): + if member.__fire_category__ not in commands_by_category: + commands_by_category[member.__fire_category__] = ActionGroup(name=member.__fire_category__, plural=member.__fire_category__) + commands_by_category[member.__fire_category__].Add(name=member_name, member=member) + else: + commands.Add(name=member_name, member=member) if value_types.IsValue(member): values.Add(name=member_name, member=member) @@ -396,7 +457,9 @@ def _GetActionsGroupedByKind(component, verbose=False): else: indexes.Add(name=f'0..{component_len-1}') - return [groups, commands, values, indexes] + + + return [groups] + list(groups_by_category.values()) + [commands] + list(commands_by_category.values()) + [values, indexes] def _GetCurrentCommand(trace=None, include_separators=True):