import logging import click logger = logging.getLogger(__name__) # Click Plugins # =============================== class NestedHelpGroup(click.Group): # class NestedHelpGroup(RichGroup): """This class provides a way to show all commands of all children instead of just the parent. Optionnaly accept to hides groups or not. This class is aimed to serve simple resource based CLIs, à la [cliff](), but with the click library. This class provides: * Recursive command listing * Command aliasing as described in (documentation)[https://click.palletsprojects.com/en/8.1.x/advanced/#command-aliases] * Rich support (TODO, more complete wrapper of click-rich) * Basic views (TODO) # Recursive command list # ============= Recursive listing on help commands, this is helpful to have an quick overview of all commands on small clis. It also provides an option to hide groups if you don't use them, which may reduce the size of the help message in case of many groups. # Command shortcuts # ============= Let's image a command line that provides: ``` myapp user show myapp user state myapp user disable ``` You could use as shortcuts: ``` mysapp u sh USER mysapp u st USER mysapp u sd USER ``` But `mysapp u s USER` would fail as it would not know if it have to redirect to `show` or `state` command, as both starts with `s` """ # For partial name resolution def resolve_command(self, ctx, args): "Return the full command name if changed" _, cmd, args = super().resolve_command(ctx, args) if _ != cmd.name: logger.debug(f"Rewrite command '{_}' to '{cmd.name}'") return cmd.name, cmd, args def get_command(self, ctx, cmd_name: str): """Given a context and a command name, this returns a :class:`Command` object if it exists or returns ``None``. """ # Resolve name part by part parts = cmd_name.split(" ") len_parts = len(parts) - 1 curr = self for idx, part in enumerate(parts): match = curr.commands.get(part) # Look for shortcut if last part if match is None: # Look for direct children only matches = [ x for x in self._resolve_children( self, ctx=ctx, ignore_groups=False, deep=0 ) if x.startswith(cmd_name) ] # Look for possible matches if not matches: pass elif len(matches) == 1: match = click.Group.get_command(self, ctx, matches[0]) else: ctx.fail( f"Too many matches for {cmd_name}: {', '.join(sorted(matches))}" ) # Iterate over next child! curr = match return curr def list_commands(self, ctx) -> list[str]: "List all children commands" return sorted(self._resolve_children(self, ctx=ctx, ignore_groups=True)) @classmethod def _resolve_children( cls, obj, ctx=None, ignore_groups=False, _parent=None, deep=-1 ): "Resolve recursively all children" # Source: Adapted from https://stackoverflow.com/a/56159096 if isinstance(obj, click.Group): ret = [] for name, child in obj.commands.items(): # Build full name full_name = name if _parent: full_name = " ".join([_parent, name]) # Save records record = full_name if ignore_groups: if not isinstance(child, click.Group): ret.append(record) else: ret.append(record) # Recursive loop if deep != 0: deep = deep - 1 ret.extend( cls._resolve_children( child, ctx=ctx, ignore_groups=ignore_groups, _parent=full_name, deep=deep, ) ) return ret return []