diff --git a/src/nomad_tools/aliasedlazygroup.py b/src/nomad_tools/aliasedlazygroup.py new file mode 100644 index 0000000..63b9bc8 --- /dev/null +++ b/src/nomad_tools/aliasedlazygroup.py @@ -0,0 +1,51 @@ +# https://click.palletsprojects.com/en/8.1.x/complex/#defining-the-lazy-group +import importlib +import click + + +class AliasedLazyGroup(click.Group): + def __init__(self, *args, lazy_subcommands=None, **kwargs): + super().__init__(*args, **kwargs) + # lazy_subcommands is a map of the form: + # + # {command-name} -> {module-name}.{command-object-name} + # + self.lazy_subcommands = lazy_subcommands or {} + + def list_commands(self, ctx): + base = super().list_commands(ctx) + lazy = sorted(self.lazy_subcommands.keys()) + return base + lazy + + def _lazy_load(self, cmd_name): + # lazily loading a command, first get the module name and attribute name + import_path = self.lazy_subcommands[cmd_name] + modname, cmd_object_name = import_path.rsplit(".", 1) + # do the import + mod = importlib.import_module(modname) + # get the Command object from that module + cmd_object = getattr(mod, cmd_object_name) + # check the result to make debugging easier + if not isinstance(cmd_object, click.Command): + raise ValueError( + f"Lazy loading of {import_path} failed by returning " + "a non-command object" + ) + return cmd_object + + def get_command(self, ctx, cmd_name): + rv = click.Group.get_command(self, ctx, cmd_name) + if rv is not None: + return rv + matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)] + if not matches: + return None + elif len(matches) != 1: + ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") + return self._lazy_load(matches[0]) + + def resolve_command(self, ctx, args): + # always return the full command name + _, cmd, args = super().resolve_command(ctx, args) + assert cmd + return cmd.name, cmd, args diff --git a/src/nomad_tools/common_click.py b/src/nomad_tools/common_click.py index f885399..a945d8b 100644 --- a/src/nomad_tools/common_click.py +++ b/src/nomad_tools/common_click.py @@ -71,10 +71,8 @@ def main_options(): ) -def common_options(): - return composed( - click.help_option("-h", "--help"), - ) +def help_h_option(): + return click.help_option("-h", "--help") def verbose_option(): diff --git a/src/nomad_tools/entry.py b/src/nomad_tools/entry.py index 1e5d411..d17ff44 100644 --- a/src/nomad_tools/entry.py +++ b/src/nomad_tools/entry.py @@ -3,26 +3,9 @@ import clickforward from click.shell_completion import BashComplete -from . import ( - entry_constrainteval, - entry_cp, - entry_dockers, - entry_downloadrelease, - entry_githubrunner, - entry_gitlab_runner, - entry_go, - entry_listattributes, - entry_listnodeattributes, - entry_nodenametoid, - entry_port, - entry_task, - entry_info, - entry_vardir, - entry_watch, -) -from .common_click import EPILOG, common_options, main_options +from .aliasedlazygroup import AliasedLazyGroup +from .common_click import EPILOG, help_h_option, main_options from .common_nomad import namespace_option -from .aliasedgroup import AliasedGroup clickforward.init() @@ -60,37 +43,39 @@ %(complete_func)s_setup; """ +subcommands = """ + constrainteval + cp + dockers + downloadrelease + githubrunner + gitlab-runner + go + info + listattributes + listnodeattributes + nodenametoid + port + task + vardir + watch + """.split() + @click.command( "nomadtools", - cls=AliasedGroup, + cls=AliasedLazyGroup, + lazy_subcommands={cmd: f"{__package__}.entry_{cmd.replace('-', '_')}.cli" for cmd in subcommands}, help="Collection of useful tools for HashiCorp Nomad.", epilog=EPILOG, ) @namespace_option() -@common_options() +@help_h_option() @main_options() def cli(): pass -cli.add_command(entry_constrainteval.cli) -cli.add_command(entry_cp.cli) -cli.add_command(entry_dockers.cli) -cli.add_command(entry_downloadrelease.cli) -cli.add_command(entry_githubrunner.cli) -cli.add_command(entry_gitlab_runner.cli) -cli.add_command(entry_go.cli) -cli.add_command(entry_listattributes.cli) -cli.add_command(entry_listnodeattributes.cli) -cli.add_command(entry_nodenametoid.cli) -cli.add_command(entry_port.cli) -cli.add_command(entry_info.cli) -cli.add_command(entry_task.cli) -cli.add_command(entry_vardir.cli) -cli.add_command(entry_watch.cli) - - def main(): cli(max_content_width=9999) diff --git a/src/nomad_tools/entry_constrainteval.py b/src/nomad_tools/entry_constrainteval.py index 767b8d2..d02f534 100644 --- a/src/nomad_tools/entry_constrainteval.py +++ b/src/nomad_tools/entry_constrainteval.py @@ -18,7 +18,7 @@ from packaging.version import Version from .common import mynomad -from .common_click import EPILOG, common_options, verbose_option +from .common_click import EPILOG, help_h_option, verbose_option from .mytabulate import mytabulate log = logging.getLogger(__name__) @@ -326,7 +326,7 @@ def grouper(thelist: List[str], count: int) -> List[List[str]]: @clickdc.adddc("args", NodeCacheArgs) @clickdc.adddc("constraintsargs", ConstraintArgs) @verbose_option() -@common_options() +@help_h_option() def cli(args: NodeCacheArgs, constraintsargs: ConstraintArgs): return main(args, constraintsargs) diff --git a/src/nomad_tools/entry_cp.py b/src/nomad_tools/entry_cp.py index ab9fad2..764a588 100755 --- a/src/nomad_tools/entry_cp.py +++ b/src/nomad_tools/entry_cp.py @@ -29,7 +29,7 @@ from . import nomadlib, taskexec from .common import ( cached_property, - common_options, + help_h_option, mynomad, namespace_option, nomad_find_job, @@ -814,7 +814,7 @@ class Args: """, ) @namespace_option() -@common_options() +@help_h_option() @clickdc.adddc("args", Args) def cli(args: Args): global ARGS diff --git a/src/nomad_tools/entry_dockers.py b/src/nomad_tools/entry_dockers.py index 1136067..c21c7d7 100644 --- a/src/nomad_tools/entry_dockers.py +++ b/src/nomad_tools/entry_dockers.py @@ -81,7 +81,7 @@ class Args: `nomadtools dockers ./file.nomad.hcl | xargs docker pull`. """, ) -@common.common_options() +@common.help_h_option() @clickdc.adddc("args", Args) def cli(args: Args): logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) diff --git a/src/nomad_tools/entry_downloadrelease.py b/src/nomad_tools/entry_downloadrelease.py index c54e648..9d3e2bd 100644 --- a/src/nomad_tools/entry_downloadrelease.py +++ b/src/nomad_tools/entry_downloadrelease.py @@ -13,7 +13,7 @@ import click import requests -from .common_click import common_options +from .common_click import help_h_option log = logging.getLogger(__name__) @@ -85,7 +85,7 @@ def get_arch() -> str: is_flag=True, help="Instead of downloading, only show the chosen version", ) -@common_options() +@help_h_option() def cli( pinversion: Optional[str], arch: str, diff --git a/src/nomad_tools/entry_githubrunner.py b/src/nomad_tools/entry_githubrunner.py index 8d2cea8..cc30a4b 100644 --- a/src/nomad_tools/entry_githubrunner.py +++ b/src/nomad_tools/entry_githubrunner.py @@ -41,7 +41,7 @@ from . import nomadlib from .aliasedgroup import AliasedGroup from .common import mynomad -from .common_click import common_options +from .common_click import help_h_option from .nomadlib.connection import urlquote from .nomadlib.datadict import DataDict @@ -1182,7 +1182,7 @@ class Args: @click.command("githubrunner", cls=AliasedGroup) @clickdc.adddc("args", Args) -@common_options() +@help_h_option() def cli(args: Args): global ARGS ARGS = args diff --git a/src/nomad_tools/entry_gitlab_runner.py b/src/nomad_tools/entry_gitlab_runner.py index 7817b8e..0b339f0 100755 --- a/src/nomad_tools/entry_gitlab_runner.py +++ b/src/nomad_tools/entry_gitlab_runner.py @@ -21,7 +21,7 @@ from . import entry_watch, nomadlib, taskexec from .aliasedgroup import AliasedGroup -from .common import (cached_property, common_options, get_package_file, +from .common import (cached_property, help_h_option, get_package_file, get_version, mynomad, quotearr) from .nomadlib.datadict import DataDict from .nomadlib.types import Job, JobTask, JobTaskConfig @@ -679,7 +679,7 @@ class BuildFailure(Exception): envvar="CUSTOM_ENV_CI_RUNNER_ID", show_default=True, ) -@common_options() +@help_h_option() def cli(verbose: int, configpath: Path, runner_id: int): # Read configuration configcontent = configpath.read_text() diff --git a/src/nomad_tools/entry_go.py b/src/nomad_tools/entry_go.py index 5f800a2..94624c7 100644 --- a/src/nomad_tools/entry_go.py +++ b/src/nomad_tools/entry_go.py @@ -23,7 +23,7 @@ from . import entry_watch, taskexec from .common_base import NOMAD_NAMESPACE, quotearr -from .common_click import EPILOG, common_options +from .common_click import EPILOG, help_h_option from .common_nomad import namespace_option clickforward.init() @@ -632,7 +632,7 @@ def __thread(self): """ f"{EPILOG}", ) -@common_options() +@help_h_option() @namespace_option() @clickdc.adddc("args", Args) @clickdc.adddc("notifyargs", entry_watch.NotifyOptions) diff --git a/src/nomad_tools/entry_info.py b/src/nomad_tools/entry_info.py index 3e59558..1505eb4 100644 --- a/src/nomad_tools/entry_info.py +++ b/src/nomad_tools/entry_info.py @@ -17,7 +17,7 @@ cls=AliasedGroup, help="Get information about current Nomad state", ) -@common_click.common_options() +@common_click.help_h_option() @common_click.verbose_option() def cli(): pass diff --git a/src/nomad_tools/entry_listattributes.py b/src/nomad_tools/entry_listattributes.py index 6908a16..e075e68 100644 --- a/src/nomad_tools/entry_listattributes.py +++ b/src/nomad_tools/entry_listattributes.py @@ -8,7 +8,7 @@ import clickdc from . import entry_constrainteval -from .common import common_options, verbose_option +from .common import help_h_option, verbose_option from .common_click import completor from .entry_constrainteval import ConstraintArgs, NodeCacheArgs, NodesAttributes @@ -37,7 +37,7 @@ def get_all_nodes_attributes(args: Optional[NodeCacheArgs] = None): ) @clickdc.adddc("args", NodeCacheArgs) @verbose_option() -@common_options() +@help_h_option() def cli(args: NodeCacheArgs, attributes: Tuple[str, ...]): logging.basicConfig() if attributes: diff --git a/src/nomad_tools/entry_listnodeattributes.py b/src/nomad_tools/entry_listnodeattributes.py index dc80246..798cb6d 100644 --- a/src/nomad_tools/entry_listnodeattributes.py +++ b/src/nomad_tools/entry_listnodeattributes.py @@ -7,7 +7,7 @@ import click import clickdc -from .common import common_options, mynomad, verbose_option +from .common import help_h_option, mynomad, verbose_option from .common_click import completor from .entry_constrainteval import NodeCacheArgs, NodesAttributes from .mytabulate import mytabulate @@ -32,7 +32,7 @@ def get_all_node_names(): ) @clickdc.adddc("args", NodeCacheArgs) @verbose_option() -@common_options() +@help_h_option() def cli(args: NodeCacheArgs, nodenameorid: Tuple[str, ...]): logging.basicConfig() nodesattributes = NodesAttributes.load(args) diff --git a/src/nomad_tools/entry_nodenametoid.py b/src/nomad_tools/entry_nodenametoid.py index bc6e756..7f0d1e3 100644 --- a/src/nomad_tools/entry_nodenametoid.py +++ b/src/nomad_tools/entry_nodenametoid.py @@ -5,7 +5,7 @@ import click from .common import mynomad -from .common_click import common_options, completor +from .common_click import help_h_option, completor def get_nodenametoid() -> Dict[str, str]: @@ -29,7 +29,7 @@ def get_nodenametoid() -> Dict[str, str]: required=True, shell_complete=completor(lambda: list(get_nodenametoid().keys())), ) -@common_options() +@help_h_option() def cli(prefix: bool, nodename: Tuple[str, ...]): nodenametoid = get_nodenametoid() for name in nodename: diff --git a/src/nomad_tools/entry_port.py b/src/nomad_tools/entry_port.py index 1f58e57..43381f8 100644 --- a/src/nomad_tools/entry_port.py +++ b/src/nomad_tools/entry_port.py @@ -12,7 +12,7 @@ from . import nomadlib from .common import ( alias_option, - common_options, + help_h_option, mynomad, namespace_option, nomad_find_job, @@ -135,7 +135,7 @@ def id_completor( type=re.compile, help="Show only ports which name matches this regex.", ) -@common_options() +@help_h_option() @namespace_option() @click.argument("id", shell_complete=id_completor) @click.argument("label", required=False) diff --git a/src/nomad_tools/entry_task.py b/src/nomad_tools/entry_task.py index 1893fd5..e5a6135 100644 --- a/src/nomad_tools/entry_task.py +++ b/src/nomad_tools/entry_task.py @@ -267,7 +267,7 @@ def find_tasks(self) -> List[TaskAlloc]: """, ) @clickdc.adddc("findtask", FindTask) -@common_click.common_options() +@common_click.help_h_option() @common_click.verbose_option() def cli(findtask: FindTask): logging.basicConfig() @@ -333,7 +333,7 @@ def run(self, taskalloc: TaskAlloc): nomadtools task -j mail exec bash -l """, ) -@common_click.common_options() +@common_click.help_h_option() @clickdc.adddc("cmd", Cmd) def mode_exec(cmd: Cmd): for t in FINDTASK.find_tasks(): @@ -352,7 +352,7 @@ def mode_exec(cmd: Cmd): nomad alloc logs $(nomadtools task -j mail xargs) -stderr """, ) -@common_click.common_options() +@common_click.help_h_option() @click.option("-0", "--zero", is_flag=True) @click.argument("args", nargs=-1, type=clickforward.FORWARD) def mode_xargs(zero: bool, args: Tuple[str, ...]): @@ -368,7 +368,7 @@ def mode_xargs(zero: bool, args: Tuple[str, ...]): "json", help="Output found allocations and task names in json form", ) -@common_click.common_options() +@common_click.help_h_option() def mode_json(): for t in FINDTASK.find_tasks(): print(json.dumps(t.asdict())) @@ -378,7 +378,7 @@ def mode_json(): "ls", help="Output found allocations and task names", ) -@common_click.common_options() +@common_click.help_h_option() def mode_ls(): for t in FINDTASK.find_tasks(): print(t.alloc.ID, t.task) @@ -409,7 +409,7 @@ def task_path_completor( nomadtools cp "$(nomadtools task -j mail path /etc/fstab)" ./fstab """, ) -@common_click.common_options() +@common_click.help_h_option() @click.argument("path", default="", shell_complete=task_path_completor) def mode_print(path: str): for t in FINDTASK.find_tasks(): diff --git a/src/nomad_tools/entry_vardir.py b/src/nomad_tools/entry_vardir.py index adf1394..157ccff 100755 --- a/src/nomad_tools/entry_vardir.py +++ b/src/nomad_tools/entry_vardir.py @@ -20,7 +20,7 @@ from . import nomadlib from .aliasedgroup import AliasedGroup -from .common import andjoin, common_options, mynomad, namespace_option +from .common import andjoin, help_h_option, mynomad, namespace_option from .nomadlib.connection import VariableConflict log = logging.getLogger(__file__) @@ -404,7 +404,7 @@ def click_vardir_paths(required: bool = False): ) @click.option("-v", "--verbose", is_flag=True) @namespace_option() -@common_options() +@help_h_option() @click.option( "--test", type=click.Path(dir_okay=False, writable=True, path_type=Path), diff --git a/src/nomad_tools/entry_watch.py b/src/nomad_tools/entry_watch.py index 7450493..e991cff 100755 --- a/src/nomad_tools/entry_watch.py +++ b/src/nomad_tools/entry_watch.py @@ -46,7 +46,7 @@ from . import colors, exit_on_thread_exception, flagdebug, nomaddbjob, nomadlib from .common_base import andjoin, cached_property, composed, eprint -from .common_click import common_options, complete_set_namespace +from .common_click import help_h_option, complete_set_namespace from .common_nomad import ( NoJobFound, complete_job, @@ -1967,7 +1967,7 @@ class Args(LogOptions, NotifyOptions): @flagdebug.click_debug_option("NOMADTOOLS_DEBUG") @clickdc.adddc("args", Args) @namespace_option() -@common_options() +@help_h_option() def cli(args: Args): signal.signal(signal.SIGUSR1, print_all_threads_stacktrace) exit_on_thread_exception.install()