Search code examples
pythoncommand-line-interfacepython-click

Share a sub-command among multiple top level commands


I have a Python project with multiple top level entry points, let's call them foo and bar. Both entry points use the same code base, and I wish to implement a foo version and bar version command using the exact same code implementing the version() command.

So, in foo.py, I'd say

import click

@click.group()
def foo():

@foo.command
@option('--long', ...)
@option('--check', ...)
def version(long, check):
    ...

Same in bar. How can I avoid duplicating the command definitions in both files?


Solution

  • Referencing this answer, you can build a click command and add it to multiple groups or commands as two separate operations.

    Build a click command:

    First we need to build a click command. We will use a helper function to decorate the importable command like:

    import click
    
    def importable_command(*args, **kwargs):
        def decorator(f):
            return click.decorators.command(*args, **kwargs)(f)
        return decorator
    
    
    @importable_command('version')
    @click.option('--long')
    @click.option('--check')
    def version_cmd(long, check):
        click.echo('version long: {}'.format(long))
        click.echo('version check: {}'.format(check))
    

    Add command to group:

    Then we can import the command and add it to a group with:

    from where-ever-we-defined-it import version_cmd
    
    # Add our version command to this group
    a_group.add_command(version_cmd)
    

    Test Code:

    import click
    
    def importable_command(*args, **kwargs):
        def decorator(f):
            return click.decorators.command(*args, **kwargs)(f)
        return decorator
    
    
    @importable_command('version')
    @click.option('--long')
    @click.option('--check')
    def version_cmd(long, check):
        click.echo('version long: {}'.format(long))
        click.echo('version check: {}'.format(check))
    
    
    @click.group()
    def cli():
        """An Awesome CLI"""
    
    
    # Add our version command to this group
    cli.add_command(version_cmd)
    
    if __name__ == "__main__":
        commands = (
            'version --long a_long',
            'version --help',
            '--help',
        )
    
        import sys, time
    
        time.sleep(1)
        print('Click Version: {}'.format(click.__version__))
        print('Python Version: {}'.format(sys.version))
        for cmd in commands:
            try:
                time.sleep(0.1)
                print('-----------')
                print('> ' + cmd)
                time.sleep(0.1)
                cli(cmd.split())
    
            except BaseException as exc:
                if str(exc) != '0' and \
                        not isinstance(exc, (click.ClickException, SystemExit)):
                    raise
    

    Results:

    Click Version: 6.7
    Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
    -----------
    > version --long a_long
    version long: a_long
    version check: None
    -----------
    > version --help
    Usage: test.py version [OPTIONS]
    
    Options:
      --long TEXT
      --check TEXT
      --help        Show this message and exit.
    -----------
    > --help
    Usage: test.py [OPTIONS] COMMAND [ARGS]...
    
      An Awesome CLI
    
    Options:
      --help  Show this message and exit.
    
    Commands:
      version