3

How to use click to write a command line prompt which looks like:

choose name:
[1] Karen
[2] Bob
[3] Jo
[4] Steve

And calls a function such as:

def print_function(name):
    if name == 'Karen':
        print('CEO')
    elif name == 'Bob':
        print('CFO')
    elif name == 'Jo':
        print('COO')
    elif name == 'Steve':
        print('CIO')
    else:
        raise ValueError('Wrong name')

In summary, how to create a command prompt which shows the user options and allows them to enter integers to select the corresponding to those options?

Also, how to make the command prompt optional if --name Bob is used when calling the function from command line?

user2314737
  • 27,088
  • 20
  • 102
  • 114
Greg
  • 8,175
  • 16
  • 72
  • 125

5 Answers5

2

Using a custom class which inherits from click.Option, you can intercept the option processing and display the desired menu, and then validate the response like:

Custom Class

import click

class EnumMenuPromptFromDict(click.Option):

    def __init__(self, *args, **kwargs):
        super(EnumMenuPromptFromDict, self).__init__(*args, **kwargs)
        if 'prompt' not in kwargs:
            raise TypeError(
                "'prompt' keyword required for '{}' option".format(
                    args[0][0]))

        self.choices_dict = self.prompt
        self.prompt_menu = '\n'.join('[{}] {}'.format(i + 1, name)
                                     for i, name in enumerate(self.prompt))
        self.prompt = 'Choose from,\n{}\n{}'.format(
            self.prompt_menu, self.name)

    def prompt_for_value(self, ctx):
        """Get entered value and then validate"""
        while True:
            value = super(EnumMenuPromptFromDict, self).prompt_for_value(ctx)
            try:
                choice = int(value)
                if choice > 0:
                    return list(self.choices_dict)[choice - 1]
            except (ValueError, IndexError):
                if value in self.choices_dict:
                    return value
            click.echo('Error: {} is not a valid choice'.format(value))

    def full_process_value(self, ctx, value):
        """Convert the entered value to the value from the choices dict"""
        value = super(EnumMenuPromptFromDict, self).full_process_value(
            ctx, value)
        try:
            return self.choices_dict[value]
        except (KeyError, IndexError):
            raise click.UsageError(
                "'{}' is not a valid choice".format(value), ctx)

Using Custom Class:

To use the custom class, pass the cls parameter to @click.option() decorator like:

@click.option('--name', cls=EnumMenuPromptFromDict, prompt=titles)

where the prompt is a dict of choices like:

titles = OrderedDict((
    ('Karen', 'CEO'),
    ('Bob', 'CFO'),
    ('Jo', 'COO'),
    ('Steve', 'CIO')
))

How does this work?

This works because click is a well designed OO framework. The @click.option() decorator usually instantiates a click.Option object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Option in our own class and over ride the desired methods.

In this case we over ride click.Option.prompt_for_value() to intercept the command processing and allow the Menu number or Value to be entered. We also over ride click.Option.full_process_value() to convert the name into the title.

Test Code:

from collections import OrderedDict
titles = OrderedDict((
    ('Karen', 'CEO'),
    ('Bob', 'CFO'),
    ('Jo', 'COO'),
    ('Steve', 'CIO')
))

@click.command()
@click.option('--name', cls=EnumMenuPromptFromDict, prompt=titles)
def cli(name):
    """The Great CLI APP"""
    click.echo('a title: %s' % name)

if __name__ == "__main__":
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    cli([])
Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
-2

Well, I'm not the right guy to ask about the arguments thing (sys.argv would be a good place to start), but I CAN help you with your first problem:

choice = int (input ("[1] Karen \n[2] Bob \n[3] Jo \n[4] Steve\n")) - 1
positions = ["CEO", "CFO", "COO", "CIO"]
print (positions [choice])

This basically takes advantage of ordered lists (and subtracts 1 from the input because lists start at 0)

Levi Lesches
  • 1,508
  • 1
  • 13
  • 23
-2

You can use argparse to manually build it.

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--name")
args = parser.parse_args()


def print_function(name):
    if name == 'Karen':
        print('CEO')
    elif name == 'Bob':
        print('CFO')
    elif name == 'Jo':
        print('COO')
    elif name == 'Steve':
        print('CIO')
    else:
        raise ValueError('Wrong name')


if args.name:
    print_function(args.name)
else:
    name = input("""
choose name:
[1] Karen
[2] Bob
[3] Jo
[4] Steve
    """.strip())
    print_function(name)
Sraw
  • 18,892
  • 11
  • 54
  • 87
-2

This is easy to do with a dictionary

names = {
    1: 'Karen',
    2: 'Bob',
    3: 'Jo',
    4: 'Steve'
}

Make the prompt using string.format() and iterating over the dictionary's items:

str_items = ['[{}] {}\n'.format(val, name) for val, name in names.items()]
prompt = ''.join(str_items)
val = input('Choose name:\n' + prompt))

Then just convert the user input to int, and look up the value in the dictionary

name = names[int(val)]

print('You chose: ' + name)

then do it again for titles

titles = {
    'Karen': 'CEO',
    'Bob': 'CFO',
    'Jo': 'COO',
    'Steve': 'CIO'
}

print(titles[name])
evamicur
  • 403
  • 3
  • 8
-2

It would be nice to have just one structure holding the data (names and job titles) instead of two. Just for maintainability when people change job titles or join or leave, then you just have one thing to update instead of two (list of names + list of titles, or dict of names and dict of titles).

One way is with a list of tuples:

people = [("Karen", "CEO"), ("Bob", "CFO"), ("Jo", "COO"), ("Steve", "CIO")]

Then getting a person by name is simple:

def get_person_by_name(name):
    for person in people:
        if person[0] == name:
            return person

You don't say how many other command line parameters you need to accept apart from --name, but if it's just this one then you can do this:

if len(sys.argv) == 3 and sys.argv[1] == "--name":
    person_name = sys.argv[2]
    person = get_person_by_name(person_name)
    if not person:
        print("Person '%s' not found" % person_name)

Then if you haven't got a person yet (no parameter was given, or a name was given which wasn't found), then you can use input:

def ask_for_person():
    '''Ask which person the user wants'''
    prompt = "\n".join(["[%d] %s" % (num+1, person[0]) for num, person in enumerate(people)])

    while True:
        try:
            person_num = int(input("choose name:\n" + prompt + "\n? "))
            if person_num >=1 and person_num <= len(people):
                return people[person_num - 1]
        except ValueError:
            pass
        print("try again!")

And then finally you can print your output:

if not person:
    person = ask_for_person()
print("'%s' is the '%s'" % person)
Constance
  • 202
  • 2
  • 8