Getting Started

Welcome to Action Completer!
This page should hopefully provide you with enough information to get you started
with defining actions, groups, and parameters for use with prompt-toolkit.

Installation and Setup

Installing the package should be super duper simple as we utilize Python’s setuptools.

$ poetry add prompt-toolkit-action-completer
$ # or if you're old school...
$ pip install prompt-toolkit-action-completer

Or you can build and install the package from the git repo.

$ git clone https://github.com/stephen-bunn/prompt-toolkit-action-completer.git
$ cd ./prompt-toolkit-action-completer
$ poetry build
$ pip install ./dist/*.whl

Usage

This package supplies both a ActionCompleter and a ActionValidator for direct use with prompt-toolkit. You should be passing instances of the ActionCompleter directly to the call to prompt() as the completer keyword argument.

from prompt_toolkit.shortcuts import prompt
from action_completer import ActionCompleter
completer = ActionCompleter()

prompt(">>> ", completer=completer)

Defining Actions

This of course isn’t very useful right now since we haven’t registered any actions to the completer. We can easily add a simple hello action by decorating a callable before we make the call to prompt():

from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter

completer = ActionCompleter()


@completer.action("hello")
def _hello_action():
    print("Hello, World!")


prompt(">>> ", completer=completer)

With this little bit of logic we will automatically get hello completions from our prompt call.

_images/000-simple-hello-completion.gif

Still not very useful though. We really want to be able to determine what action to execute based on the output of our prompt call. Luckily, the completer can take whatever output the prompt has produced and determine what action should be called.

We do this by using a new method on the completer, run_action(). We can simply give this method the text produced by the prompt and it will do it’s best to execute the desired callable and return whatever value the registered action returns:

from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter

completer = ActionCompleter()


@completer.action("hello")
def _hello_action():
    print("Hello, World!")


prompt_result = prompt(">>> ", completer=completer)
completer.run_action(prompt_result)
_images/001-simple-hello-completion.gif

Now we have something that is fairly useful. Automatic completion and execution of some registered callable. However, we will start to run into issues with the execution of the action callable when the user starts providing inputs that the callable either isn’t expecting or can’t handle. To protect against this, we can use the ActionValidator to validate the prompt buffer state before we attempt to execute the action.

Because the validator is a custom validator that depends on the state of the completer, it’s recommended that you use another little helper method accessible right off of the completer instance, get_validator(). This helper method will give you a new instance of ActionValidator that will be able to check that the current prompt can be adequately handled by the registered action.

from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter

completer = ActionCompleter()


@completer.action("hello")
def _hello_action():
    print("Hello, World!")


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
completer.run_action(prompt_result)
_images/002-simple-hello-completion.gif

That is the very basics of using the ActionCompleter, with this you can easily get started creating some basic tools. But we can get a lot more detailed and provide even more useful features by also providing completion for action parameters!

Parameter Completion

These actions won’t be terribly useful unless we can supply some user specific inputs. In order to help complete these parameters we need to also register the completable parameters with the action we decorated. We can easily do this using the param() decorator:

@completer.action("hello")
@completer.param(None)
def _hello_action(name: str):
   print(f"Hello, {name!s}!")

prompt(">>> ", completer=completer)
_images/003-simple-hello-name-parameter.gif

You can now see that we are allowing for a parameter that is automatically provided to the executed callable as the first parameter! We are giving the param() decorator a source of None to indicate that we don’t necessarily want completion enabled for this parameter.

Completion Sources

There are many different available sources (some more useful than others). These completion sources are the powerhouse of how parameters are selected by the user.

None (None)

None completions indicate that a parameter is required and gives you all the nifty features of an action parameter, but doesn’t attempt to do any real completion.

Warning

A fairly big caveat of parameter completion for None source inputs, is that we don’t support values containing spaces as properly handled inputs by the user. Values containing spaces are very difficult to distinguish from upcoming parameters. We do support the ability for users to escape spaces to include them in their parameter input.

>>> hello Stephen\ Bunn
Hello, Stephen Bunn!

However, this is fairly tedious and error-prone for the customer, so we recommend that you use other action parameter completion sources if you intend for the completable values to contain spaces.

Basic (str)

A basic string can be provided as a completion source that will force the value of the parameter to always be whatever the value of this string is. Might not seem very useful, but I’ve run into several situations when building dynamic actions that this can help out with.

@completer.action("hello")
@completer.param("world")
def _hello_world_action(value: str):
   print(f"Hello, {value.title()!s}")
Iterable (List[str])

A list of strings can be provided to allow for multiple selections the user can make for the parameter. As long as the value they give is in this list of strings, the input will be considered valid and will be passed through to the action.

This source is a good substitute if you want to be able to complete enum.Enum value and cast the value back to the enum instance in the action. Since prompt deals only with strings (not enum values), it is easier for you to handle that decomposition and casting from an enum yourself.

@completer.action("hello")
@completer.param(["Mark", "John", "William"])
def _hello_person_action(name: str):
   print(f"Hello, {name!s}")
Completer (Completer)

One of the more useful parameter sources is another Completer instance. We will give the appropriate parameter value to the nested completer and any completion that the completer determines should be yielded will be yielded.

from prompt_toolkit.completion import PathCompleter

@completer.action("cat")
@completer.param(PathCompleter())
def _cat_action(filepath: str):
   with open(filepath, "r") as file_handle:
      print(file_handle.read())
Callable (Callable[[Action, ActionParam, str], Iterable[str]])

Another useful parameter source is just a custom callable. This callable should return some kind of iterable of strings that should be considered completion results. As inputs, this callable will take the following positional arguments:

  • Action (Action) - The action that is being triggered

  • ActionParam (ActionParam) - The associated action parameter that is requesting completions

  • str (str) - The current value of the action parameter

Whatever list, tuple, or generator of strings is returned will be used as the completion results.

def _get_completions(action: Action, param: ActionParam, value: sr) -> Iterable[str]:
   return [str(value) for value in range(12)]

@completer.action("hello")
@completer.param(_get_completions)
def _hello_dynamic_action(dynamic_value: str):
   print(f"Hello, {dynamic_value!s}")

Parameter Casting

It gets pretty tedious to have to manually cast our parameter results from strings into custom data types in the action itself. So give the cast keyword argument to the param() decorator and we will do it for you automatically.

@completer.action("x2")
@completer.param(["1", "2", "3"], cast=int)
def _x2_action(num: int):
   print(f"Number {num!s} times 2 is {num * 2!s}")

Note, that we don’t do anything clever when casting this value. If you request us to cast the parameter to an int and the string contains alpha characters, it will fail with the traditional ValueError. To avoid this situation, continue reading on through to handling parameter validation.

Parameter Validation

Going back to our cat action example in Completer (Completer), we can greatly improve the usability of this by giving the action parameter some validation! Simply pass a Validator instance as a value in the validators keyword argument, and you can ensure that the completed path is an existing file.

from pathlib import Path

from prompt_toolkit.completion import PathCompleter
from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.validation import Validator

from action_completer import ActionCompleter

completer = ActionCompleter()


@completer.action("cat")
@completer.param(
    PathCompleter(),
    cast=Path,
    validators=[
        Validator.from_callable(
            lambda p: Path(p).is_file(), error_message="Path is not an existing file"
        )
    ],
)
def _cat_action(filepath: Path):
    with filepath.open("r") as file_handle:
        print(file_handle.read())


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
completer.run_action(prompt_result)
_images/004-cat-path-validation.gif

Because your parameter validation may require that you also take into consideration the context of the parameter, you can also pass a custom validator callable. Similar to the custom callable parameter completion sources available in Callable (Callable[[Action, ActionParam, str], Iterable[str]]), you can also pass a callable with a specific signature to the validators list. As inputs, this callable will take the following positional arguments:

  • ActionParam (ActionParam) - The associated action parameter that needs to be validated

  • str (str) - The current value of the action parameter

  • List[str] (List[str]) - The previously extracted prompt buffer fragments

This callable should raise a ValidationError when validation fails. For example, let’s create an action that creates a new .txt file in a specified directory and verifies that the file can safely be created:

from pathlib import Path
from typing import List

from prompt_toolkit.completion import PathCompleter
from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.validation import ValidationError, Validator

from action_completer import ActionCompleter, ActionParam

completer = ActionCompleter()


def _validate_touch(action_param: ActionParam, param_value: str, fragments: List[str]):
    dirpath = Path(fragments[0])
    filepath = dirpath.joinpath(f"{param_value!s}.txt")
    if filepath.is_file():
        raise ValidationError(message=f"File at {filepath!s} already exists")


@completer.action("touch")
@completer.param(
    PathCompleter(only_directories=True),
    cast=Path,
    validators=[
        Validator.from_callable(
            lambda p: Path(p).is_dir(), error_message="Not an existing directory"
        )
    ],
)
@completer.param(None, validators=[_validate_touch])
def _touch_txt_action(dirpath: Path, name: str):
    filepath = dirpath.joinpath(f"{name!s}.txt")
    with filepath.open("r") as file_handle:
        print(file_handle.read())


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
completer.run_action(prompt_result)
_images/005-advanced-touch-validation.gif

By default, the ActionValidator will validate that you are providing the exact amount of parameters for an action. If you need to allow for the user to enter additional text into the prompt past the defined parameters, you can set the capture_all flag on the action to True. This will disable the check for an exact number of parameters and will instead ensure that the user gives at least the number of defined parameters.

from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter

completer = ActionCompleter()


@completer.action("hello", capture_all=True)
@completer.param(["Mark", "John", "William"])
def _hello_name(name: str, *args):
    print(f"Hello, {name!s}!")
    print(f"Additional: {args!s}")


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
completer.run_action(prompt_result)
_images/006-capture-all-action.gif

Nested Groups

Likely you will run into the situation where you want to create a subgroup of actions under a specific completable name. You can do this fairly easily by making use of the group() method which will allow you to define a subgroup on an existing group.

Any related actions (or even additional nested subgroups) should be created by using the action() or group() methods from the newly created ActionGroup instance.

from pathlib import Path

from prompt_toolkit.completion import PathCompleter
from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.validation import Validator

from action_completer import ActionCompleter

completer = ActionCompleter()

hello_group = completer.group("hello")


@hello_group.action("world")
def _hello_world():
    print("Hello, World!")


@hello_group.action("custom")
@completer.param(None)
def _hello_custom(name: str):
    print(f"Hello, {name!s}!")


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
completer.run_action(prompt_result)
_images/005-nested-action-group.gif

Important

Note that the param() decorator is only available from the ActionCompleter instance. You should always be using the completers instance that is sent to the prompt() call to register any parameters for any actions (no matter how nested they are).

nested_group = completer.group("nested-group")

# Invalid, @param is not available on the group
@nested_group.action("invalid-action")
@nested_group.param(None)
def _invalid_action(invalid_param):
   ...

# Valid, always use @param from the completer
@nested_group.action("valid-action")
@completer.param(None)
def _valid_action(valid_param):
   ...

Styling Completions

Now that we have some context into how we create groups, actions, and parameters, we can talk about customizing the style of the completions. Each ActionGroup, Action, and ActionParam has the style, selected_style, display, and display_meta properties.

Color

If you want to change the coloring of the completion results, you can use the style and selected_style properties to do so. These styles are given directly to the Completion instance and will result in the completion results being styled all the same way:

from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter

completer = ActionCompleter()


@completer.action("hello")
@completer.param(
    ["Mark", "John", "William"],
    style="fg:white bg:red",
    selected_style="fg:red bg:white bold",
)
def _hello_name(name: str):
    print(f"Hello, {name!s}!")


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
completer.run_action(prompt_result)
_images/007-color-completions.gif

Both style and selected_style can be lazily evaluated if necessary. Simply pass a callable with a signature similar to the following:

from action_completer.types import ActionCompletable_T

def dynamic_style(completable: ActionCompletable_T, completable_value: str) -> str:
   ...

For example, let’s say we want to style the “John” completion with a blue background and leave everyone else with a red background:

from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter, types

completer = ActionCompleter()


def dynamic_style(
    completable: types.ActionCompletable_T, completable_value: str
) -> str:
    if completable_value.lower() == "john":
        return "fg:white bg:blue"

    return "fg:white bg:red"


@completer.action("hello")
@completer.param(
    ["Mark", "John", "William"],
    style=dynamic_style,
    selected_style="bold",
)
def _hello_name(name: str):
    print(f"Hello, {name!s}!")


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
completer.run_action(prompt_result)
_images/008-dynamic-color-completions.gif

Warning

Take note of the return type for the dynamic style value. This callable has the constraint that it requires we always return a string of some kind; it should never be allowed to return None. To omit from styling a completion, simply return an empty string. Typically it is safest to return early for any desired stylings you would like to apply and leave an empty string as the last available return value at the bottom of the callable:

def dynamic_style(completable: ActionCompletable_T, completable_value: str) -> str:
   # logic to determine the desired style for a completion result
   ...

   # always return AT LEAST an empty string
   return ""

Text

You can customize the actual display of the completion text itself by passing some string to the display keyword argument:

from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter

completer = ActionCompleter()


@completer.action("hello", display="Run hello world")
def _hello_world():
    print("Hello, World!")


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
completer.run_action(prompt_result)
_images/009-display-completions.gif

We can also pass an instance of FormattedText to display to get some pretty fancy completions:

from prompt_toolkit.formatted_text import HTML, to_formatted_text
from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter

completer = ActionCompleter()


@completer.action("hello", display=to_formatted_text(HTML("Run <i>hello world</i>")))
def _hello_world():
    print("Hello, World!")


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
completer.run_action(prompt_result)
_images/010-display-formattedtext-completions.gif

Similar to the style and selected_style properties, we can define the display lazily through a callable. The callable for the display should have a signature similar to the following:

from typing import Union

from prompt_toolkit.formatted_text import FormattedText
from action_completer.types import ActionCompletable_T

def dynamic_display(
   completable: ActionCompletable_T, completable_value: str
) -> Union[str, FormattedText]:
   ...

You can also easily include the completion value within a string or FormattedText instance without having to do anything fancy by supplying a {completion} format in the display or display_meta values. For example:

from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter

completer = ActionCompleter()


@completer.action("hello")
@completer.param(["1", "2", "3"], cast=int, display_meta="Says hello to {completion}")
def _hello_action(num: int):
    print(f"Hello, {num!s}!")


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
completer.run_action(prompt_result)

This will produce completions where the completed value is inserted into the template provided for the display_meta.

_images/014-display-completion-interpolation.gif

Somewhat useful if you don’t want to go through the effort of defining a callable to just place completion values in your completion’s display or descriptions.

Description

If you would like to add a little helpful description to a completion, you can do so through the display_meta keyword argument.

from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter

completer = ActionCompleter()


@completer.action("hello", display_meta="Run hello world")
def _hello_world():
    print("Hello, World!")


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
completer.run_action(prompt_result)
_images/011-display-meta-completions.gif

Everything that you can do with display, you can also do with display_meta. For more details about what kind of formats you can display, you should read through the prompt-toolkit documentation

Conditional Actions

Both Action and ActionGroup can be gated behind a Filter to indicate if the action or group should be considered completable. This filter is provided through the active keyword argument; by default it is set to None. If this filter is provided, it will be evaluated during completion and, if it evaluates to a falsy value, completion will not occur for the associated action or group.

For example, here is a quick (and dirty) method for only allowing a hello action to run once the user has first called activate:

from prompt_toolkit.filters import Condition
from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter

completer = ActionCompleter()


ACTIVE: bool = False


def is_active() -> bool:
    return ACTIVE


def set_active(state: bool) -> bool:
    global ACTIVE
    ACTIVE = state
    return ACTIVE


@completer.action("activate", active=Condition(lambda: not is_active()))
def _activate_action():
    set_active(True)


@completer.action("deactivate", active=Condition(is_active))
def _deactivate_action():
    set_active(False)


@completer.action("hello", active=Condition(is_active))
def _hello_world():
    print("Hello, World!")


while True:
    prompt_result = prompt(
        ">>> ", completer=completer, validator=completer.get_validator()
    )
    completer.run_action(prompt_result)
_images/012-conditional-actions.gif

Custom Action Execution

If you ever need to tweak how the requested action is executed, you can fetch a partial() instance from the completer instead of just calling run_action(). Use the get_partial_action() method with the output of the prompt to get an callable for the desired action with the casted parameters applied:

from prompt_toolkit.shortcuts import prompt

from action_completer import ActionCompleter

completer = ActionCompleter()


@completer.action("hello")
@completer.param(["World", "Stephen"])
def _hello_action(name: str, *args):
    print(f"Hello, {name!s}!")
    print(f"Additional: {args!s}")


prompt_result = prompt(">>> ", completer=completer, validator=completer.get_validator())
action_partial = completer.get_partial_action(prompt_result)

print(action_partial)
action_partial("I'm something new")
_images/013-custom-action-execution.gif
There you have it.

That is a basic overview of the features currently available in the ActionCompleter. To understand more about what is available to help to construct basic autocompletion for prompt interfaces, I would recommend you read through the prompt-toolkit documentation and continue on reading through the Action Completer Package internal reference docs.