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?

1 Answer 1

1

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)
Sign up to request clarification or add additional context in comments.

1 Comment

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.