python-shctl-iam/iam/lib/click_utils.py

144 lines
4.4 KiB
Python

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 <USER>
myapp user state <USER>
myapp user disable <USER>
```
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 []