144 lines
4.4 KiB
Python
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 []
|