Search code examples
pythoncommand-line-interfacepython-click

Perform different parent command actions if specific subcommand is called?


In a normal situation, my application will load a set of configuration values into the context and these will be passed to the subcommands with pass_context. There is only one situation where this will not work - the first time the application is run and the configuration hasn't been set up.

My goal is to allow the user to run one subcommand and generate the appropriate configuration so that the CLI works the rest of the time.

My cli.py code:

import sys
import click
from ruamel.yaml import YAML
from pathlib import Path

from commands.config_cmds import configcmd

MYAPP = "AwesomeCLI"

@click.group()
@click.version_option()
@click.pass_context
def cli(ctx):
    """command line application"""
    ctx.ensure_object(dict)
    ctx.obj['APPLICATION_NAME'] = MYAPP

    config_file = Path(click.get_app_dir(ctx.obj[MYAPP])) / "config.yml"
    yaml = YAML(typ="safe")
    try:
        config_yml = yaml.load(config_file)
    except FileNotFoundError:
        click.secho("Run command: awesome-cli configcmd first-run", fg='red')
        raise click.FileError(config_file.name, "Missing configuration file.")

    ctx.obj['CONFIG'] = yaml.dump(config_yml)


cli.add_command(configcmd)

My configcmd code:

@click.group()
def configcmd():
    """Manage configuration of this tool
    \f
    The configuration file is saved in $HOME/.config/awesome-cli
    """

@config.command()
@click.pass_context
def first_run(ctx):
    """
    Set up CLI configuration. 
    """
    api_key = click.prompt("Your API Key")
    # More stuff here about saving this file...

If I run python awesome-cli configcmd I receive the following error (as expected):

Run command: awesome-cli configcmd first-run
Error: Could not open file config.yml: Missing configuration file.

However, if I run that command python awesome-cli configcmd first-run I receive the same error, which not my goal. Obviously I should be getting that error with this code, but that's because I don't know how to add an exception based on the command/subcommand being called.

What do I need to add in my cli function in cli.py so that I don't try to load the configuration file if (and only if), the user is running configcmd first-run? Any other command/subcommand will require that this configuration file exists, so I want the check to remain for those.


Solution

  • To invoke some specific code prior to executing a subcommand, which is based on the specific subcommand invoked, you can look at ctx.invoked_subcommand like:

    if ctx.invoked_subcommand != 'configcmd':
    

    In your example you will need examine the ctx.invoked_subcommand at each level like:

    Test Code:

    import sys
    import click
    from ruamel.yaml import YAML
    from pathlib import Path
    
    MYAPP = "AwesomeCLI"
    
    @click.group()
    @click.pass_context
    def cli(ctx):
        """command line application"""
        ctx.ensure_object(dict)
        ctx.obj['APPLICATION_NAME'] = MYAPP
        ctx.obj['CONFIG_FILEPATH'] = Path(click.get_app_dir(MYAPP), "config.yml")
        if ctx.invoked_subcommand != 'configcmd':
            load_config(ctx)
    
    @cli.group()
    @click.pass_context
    def configcmd(ctx):
        """Configuration management for this CLI"""
        click.echo("In config")
        if ctx.invoked_subcommand != 'first-run':
            load_config(ctx)
    
    def load_config(ctx):
        yaml = YAML(typ="safe")
        try:
            config_yml = yaml.load(ctx.obj['CONFIG_FILEPATH'])
        except FileNotFoundError:
            click.secho("Run command: awesome-cli configcmd first-run",
                        fg='red')
            raise click.FileError(str(ctx.obj['CONFIG_FILEPATH']),
                                  "Missing configuration file.")
    
        ctx.obj['CONFIG'] = yaml.load(config_yml)
    
    
    @configcmd.command('first-run')
    @click.pass_context
    def first_run(ctx):
        """Set up CLI configuration."""
        click.echo("In first-run")
    
    @configcmd.command('test-cmd')
    @click.pass_context
    def test_cmd(ctx):
        """ This command will not be reachable without config file"""
        click.echo("In first-run")
    
    
    if __name__ == "__main__":
        commands = (
            'configcmd first-run',
            'configcmd test-cmd',
            'configcmd --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)]
    -----------
    > configcmd first-run
    In config
    In first-run
    -----------
    > configcmd test-cmd
    In config
    Run command: awesome-cli configcmd first-run
    Error: Could not open file C:\Users\stephen\AppData\Roaming\AwesomeCLI\config.yml: Missing configuration file.
    -----------
    > configcmd --help
    Usage: test.py configcmd [OPTIONS] COMMAND [ARGS]...
    
      Configuration management for this CLI
    
    Options:
      --help  Show this message and exit.
    
    Commands:
      first-run  Set up CLI configuration.
      test-cmd   This command will not be reachable without...
    -----------
    > --help
    Usage: test.py [OPTIONS] COMMAND [ARGS]...
    
      command line application
    
    Options:
      --help  Show this message and exit.
    
    Commands:
      configcmd  Configuration management for this CLI
    -----------
    > 
    Usage: test.py [OPTIONS] COMMAND [ARGS]...
    
      command line application
    
    Options:
      --help  Show this message and exit.
    
    Commands:
      configcmd  Configuration management for this CLI