Skip to main content

A package to tightly bind user choices to functionality. Based on python-inquirer.

Project description

python-inquirer-executor

This is a wrapper around python-inquirer. From that project's README:

So, Inquirer should ease the process of asking end user questions, parsing, validating answers, managing hierarchical prompts and providing error feedback.

This package extends this thought by building classes on top of it to create prompts that will automatically call one or more functions corresponding to the user's choice, while keeping your code nice, tidy and readable. This is achieved by facilitating the docstring of your functions as user-facing representaion of these functions.

How to use

Installation

pip install inquirer-executor

As the whole code is contained in a single file and has only one dependecy (the built-upon inquirer package), you can also manually copy/paste this (or parts of it) into your project, if that is how you roll.

Creating a single-choice question (List)

from inquirer_executor import InquirerExecutorList 

question = InquirerExecutorList("Question you want to ask the user?", list_of_functions)

The list_of_functions can be any iterable that is exlusively composed of function types. The class will generate the question from the string you provided as the first argument and a list of functions that will be presented to the user as selectable options. These options will be represented by the given function's corresponding docstring.

Example

from inquirer_executor import InquirerExecutorList 

def print_one():
    """One."""
    print("one")

def print_two():
    """Two."""
    print("two")

def print_three():
    """Three."""
    print("three")

question = InquirerExecutorList("Of the given choices, how many puppies is best?", [print_one, print_two, print_three])    

This will create the instance of the question. You now have prompt_user() and prompt_and_execute() methods at your disposal. Once you have used the prompt_user() method, and the user has provided an answer, you can also:

  • use the find_function() method to return the corresponding function to the user's answer
  • access the instances answer value to read the user's answer (the docstring they have selected as string)
  • use the execute() method to execute the users choice at a later point (the function returns the return value of the function called)

For now though, we are just going to use prompt_and_execute() to see the results right away:

question.prompt_and_execute()

Which gives us this output:

[?] Of the given choices, how many puppies is best?: Three.
   One.
   Two.
 > Three.

three

The user has chosen from the docstrings representing the functions and the function got executed, printing 'three'. Neat.

Creating a multiple-choice question (Checkbox)

from inquirer_executor import InquirerExecutorCheckbox

question = InquirerExecutorCheckbox("Question you want to ask the user?", list_of_functions)

Initializing this works exactly like it does for the InquirerExecutorList class. The difference is the existence of the execution_stack value, which is a list that contains all the options the user has checked. So this class will never return a single function, always a list of functions.

Example

from inquirer_executor import InquirerExecutorCheckbox

def print_puppies():
    """Puppies."""
    print("puppies")

def print_rocks():
    """Rocks."""
    print("rocks")

def print_kittens():
    """Kittens."""
    print("kittens")

question2 = InquirerExecutorCheckbox("Of the given choices, which ones are furry and cuddly?", [print_puppies, print_rocks, print_kittens])

This will create the instance of the question. Again, you now have prompt_user() and prompt_and_execute() methods at your disposal. Once you have generated an answer with the prompt_user() method, you can:

  • use the find_functions() (mind the plural 's') method to return the corresponding list of functions to the users answer
  • access the instances answer value to read the user's answers (a list of the docstrings they have selected)
  • use the execute() method to execute the users choices at a later point (the function itself returns a list of the functions return values)

For now though, we are again just going to use prompt_and_execute() to get this result:

[?] Of the given choices, which one's are furry and cuddly?: 
   X Puppies.
   o Rocks.
 > X Kittens.

puppies
kittens

The user has checked options one and three and the corresponing functions got called.

Keep in mind that in this case prompt_and_execute() always returns a list.

Mutating the question after instantiation

Adding

There are two ways to add functions to InquirerExecutor instances after they have been created. The first one is the + operator, that will append the added function to the end of the choices associated with the question.

The second one is the insert(index, value) method, that will insert a value (which in this case has to be a function type) at index. Use it like you are used to from the list type.

Setting

You can also set new values as you are used to like

instance[0] = new_value

where again, new_value needs to be a function type.

Reordering

InquirerExecutor provides a reorder(indices) method where indices is a list of numbers that represent the new order, so when given [2, 0, 1], the original index 0 would be moved to index 2, original index 1 moved to 0 and 2 to 1.

You can also use the reverse() method, which also works like you are used to from list types.

Removing

InquirerExecutor provides a remove(value) method, that excepts either a function name as string or an index as number as it's value argument. In both cases, the matching function is removed from the choices presented to the user.

Passing arguments

You can of course pass whatever arguments you like to your functions. Just keep in mind, that potentially any and every function in the list will be called, so all of your functions must accept the same parameters. To prevent possible errors down the road, InquirerExecuter enforces this at creation time and will throw an AssertionError if the accepted parameters of your functions don't match.

Theming

You can use python-inquirer's built-in theming options with the key difference that you have to instantiate the theme before using it. You then pass the instance to the prompt_user() or prompt_and_execute() methods using the theme keyword, not the theme class.

Dynamically setting docstrings

This package makes it sometimes necessary - or at least preferable - to generate docstrings dynamically. This could be achieved by defining the docstring after you define the function like so:

name = input("What is your name?")

def some_function():
    """Can't display the name variable here."""
    return name

some_function.__doc__ = "Returns your name: {}".format(name)

This is possible and valid as long as you are dealing with normal functions. As soon as you are trying to do this with methods inside a class, Python will raise an error telling you that the __doc__ attribute of methods is not writable.

For this reason InquirerExecutor provides a decorator named dynamic_docstring_decorator that can be used to set dynamic docstrings. The above code rewritten with the decorator would look like this:

from inquirer_executor import dynamic_docstring_decorator

name = input("What is your name?")

@dynamic_docstring_decorator("Returns your name: {}".format(name))
def some_function():
    """Can't display the name variable here.""" # This docstring gets overwritten
    return name

Much nicer and cleaner!

Using this as part of a whole catalogue of questions

Depending on what you are trying to achieve you might want to organize the questions yourself in a manner that fits your use case best. For simple applications, InquirerExecutor provides a QuestionsCatalogue class, that can be instantiated with a n iterable type that consists of either inquirer or inquirer_executor objects.

The QuestionsCatalogue handles these objects so they feel just like a list of functions and equips you with it's prompt_all() method. This method returns a tuple of two items: 1) A dictionairy of all the answers given to the Text, Path, etc. prompts that you may have used directly from inquirer and 2) a list of functions the user has chosen from single- and multiple-choice questions in the QuestionsCatalogue. In order to keep everything human-readable and easy to reason about, this class provides no way of directly calling all functions, you need to call them yourself however and whenever you see fit.

Example

from inquirer import Text
from inquirer_executor import (
    InquirerExecutorList as InqExList,
    InquirerExecutorCheckbox as InqExCheckbox,
    QuestionsCatalogue,
)

def return_one():
    """Return 1"""
    return 1


def return_two():
    """Return 2"""
    return 2


def return_three():
    """Return 3"""
    return 3


def return_four():
    """Return 4"""
    return 4


def return_five():
    """Return 5"""
    return 5


inqex_checkbox = InqExCheckbox.from_iterable(
    "What do you want to return?", [return_one, return_two]
)

inqex_list = InqExList.from_iterable(
    "What do you want to return?", [return_three, return_four, return_five]
)

text_question_first_name = Text("first_name", message="What's your first name")

text_question_last_name = Text("last_name", message="What's your last name")

questions_catalogue = QuestionsCatalogue(
    [inqex_checkbox, inqex_list, text_question_first_name, text_question_last_name]
)

print(questions_catalogue.prompt_all())

Assuming the user checked both options at the checkbox and chose "Return 4" at the single choice question and he is a billionaire from Gotham City the above code would produce something like this:

[?] What do you want to return?: 
   X Return 1
 > X Return 2

[?] What do you want to return?: Return 4
   Return 3
 > Return 4
   Return 5

[?] What's your first name: Bruce
[?] What's your last name: Wayne
({'first_name': 'Bruce', 'last_name': 'Wayne'}, [<function return_one at 0x7f516964de18>, <function return_two at 0x7f51663a4d90>, <function return_four at 0x7f516611bd08>])

Examples

If you would like to see this package applied in a bit more complex examples, please do consult the examples folder of the repository. These small projects are structured with human-readability in mind and are heavily commented to guide you through the code to get you working with this package in no time.

Raison D'être

I needed this myself.

Contributing

Contributions and improvements are very welcome. Please write a test for your code contribution and use the Black code formatter when editing the code in this project.

If you have played around with the package and you think what you have created would make a good example project, I would absolutely love to merge it into the examples folder, please make sure to comment your code so others can understand what you are doing.

License

Copyright (c) 2019 Konstantin Kovar, based on python-inquirer, by Miguel Ángel García (@magmax9).

Licensed under the MIT license.

Project details


Release history Release notifications

This version

0.1.3

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Files for inquirer-executor, version 0.1.3
Filename, size File type Python version Upload date Hashes
Filename, size inquirer_executor-0.1.3-py3-none-any.whl (9.7 kB) File type Wheel Python version py3 Upload date Hashes View hashes
Filename, size inquirer_executor-0.1.3.tar.gz (9.0 kB) File type Source Python version None Upload date Hashes View hashes

Supported by

Elastic Elastic Search Pingdom Pingdom Monitoring Google Google BigQuery Sentry Sentry Error logging AWS AWS Cloud computing DataDog DataDog Monitoring Fastly Fastly CDN DigiCert DigiCert EV certificate StatusPage StatusPage Status page