Skip to content

Added support for categorized groups and commands in helptext output #604

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added examples/categories/__init__.py
Empty file.
83 changes: 83 additions & 0 deletions examples/categories/categories.py
Original file line number Diff line number Diff line change
@@ -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()
79 changes: 71 additions & 8 deletions fire/helptext.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

import collections
import itertools
import functools

from fire import completion
from fire import custom_descriptions
Expand Down Expand Up @@ -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)))

Expand Down Expand Up @@ -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)

Expand All @@ -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):
Expand Down