0

[EDIT]

I would like to create dynamic command / argument structure from a custom python tree object.

Referencing this post I managed to properly bind the commands:

Stackoverflow: Dynamically Create click commands

The goal would be to use the bash shell to feed a command of arbitrary depth, and have the cli tab completion dynamically populated with the next level within the hierarchy along with help for their arguments ( fed in through a dictionary resolver_dict )

honeycomb content

would run the content

but:

honeycomb content asset

Would run the content asset block, but not both content then asset

I am taking care of all the argument inheritance within the honeycomb object, so I don't believe I need to forward arguments via click for this as suggested in other posts.

Share options between click commands

So, if the content block has an argument called studio_root, and the content asset has an argument called asset_name, then the I am internally forwarding ALL arguments from the parents to all their children. ie content asset will have both the studio_root and asset_name arguments already present via the resolver_dict. ( see below )

content = {'studio_root': some_value}
    asset = {'studio_root': some_value (inherited), 'asset_name': lulu}

I am closer. I am able to create / load commands & arguments with the following yaml structure:

Yaml Structure:

_content:
  studio_root: /studio
  _asset:
    asset_name: larry_boo

but with the following command:

honeycomb content asset --asset_name lulu

I am getting the following error:

$ honeycomb content asset --asset_name lulu
I am the "<content>" command
Usage: honeycomb content asset [OPTIONS] COMMAND [ARGS]...
Try 'honeycomb content asset --help' for help.

Error: Missing command.

There are 2 problems here.

  • We can think of the command honeycomb content asset as a path to a single command... not a chain of commands. I only want the final path to run: ie - honeycomb content asset should ONLY run the asset command and not the honeycomb content block. Since the click groups are nested, this might require a higher level control over the command invoking? In this case, It appears that it is first running ( honeycomb content) and then failing on the honeycomb content asset command?
  • I assume the "Missing command" is the honeycomb content asset command, but that should be: print('I am the "<asset>" command'), which is missing?

Here is the code:

DATA_PATH = '{}/configs/hive.yaml'.format(pathlib.Path(__file__).parent.parent)


@click.group()
@click.pass_context
def cli(ctx):
    pass


# bind commands and args
def bind_func(name, c, kwargs):
    def func(**kwargs):
        click.echo('I am the "<{}>" command'.format(name))
    if not kwargs:
        print('found no kwargs')
        return(func)
    # add all the key, values in node_data
    for key, value in kwargs.items():
        if key.startswith('__'):
            continue
        if key == 'command':
            continue
        #click.echo('\tadding option: {}'.format(key))
        option = click.option('--{}'.format(key, default=value))
        func = option(func)

    # rename the command and return it
    func.__name__ = name
    return func


def main():
    '''
    '''
    # Load the honeycomb object ( tree of nodes )
    hcmb = hcm.Honeycomb(DATA_PATH)

    # get the buildable nodes
    nodes = hcmb.builder.get_buildable_nodes()

    # determine the root path
    cli_groups = {'|cli': cli}

    for node in nodes:
        nice_name = node.get_name()[1:]
        path = node.get_path()
        cli_path = '|cli' + path
        parent = node.get_parent()
        if not parent:
            parent_cli_path = '|cli'
        else:
            parent_cli_path = '|cli' + parent.get_path()

        # get the node data to add the arguments:
        resolver_dict = node.get_resolver_dict()

        # create the new group in the appropriate parent group
        func = bind_func(nice_name, '_f', resolver_dict)
        new_group = cli_groups[parent_cli_path].group(name=nice_name)(func)


        # and append it to the list of groups
        cli_groups[cli_path] = new_group
    

    # now call the cli
    cli()

if __name__ == '__main__':
    main()

Sorry for the long code details, but I think it would be difficult to understand what I am doing without it. Maybe I am trying to force click to work in ways it wasn't intended?

speeders
  • 3
  • 3

1 Answers1

0

I've managed to do something like this in the fast. However, it was by creating a new decorator to contain all my options, like common_options below

import click

_global_options = [
    click.option('-v', '--verbose', count=True, default=0, help='Verbose output.'),
    click.option('-n', '--dry-run', is_flag=True, default=False, help='Dry-run mode.')
]


def common_options(func):
    for option in reversed(_global_options):
        func = option(func)
    return func

I would then apply @common_options as a decorator to my group when creating it, but I have just tried it now and, with decorators just being function wrappers, it works the following way as well:

# Standard way using it as a decorator
@click.group()
@common_options
def cli():
    pass

# Using it as a function
@click.group()
def cli():
    pass

common_options(cli)
Dharman
  • 30,962
  • 25
  • 85
  • 135
afterburner
  • 2,552
  • 13
  • 21
  • Thanks for the response! I changed the title and added some details. This is really helpful. I am slowly piecing this all together. My problem is that I don't know the options ahead of time. They are part of the honeycomb.Node object, which are children in a tree of other nodes... Any thoughts are helpful – speeders Sep 17 '20 at 23:00