Skip to content

Advanced Usage

The Overview and Getting Started pages show simplified examples of how to use columbo. These examples have consisted of:

  • statically defined list of Interactions which are then passed to get_answers() or parse_args().
  • dynamic values that were deterministic based on specific inputs

However, there are times when the actual situation is more complicated than those examples. To handle these situations there are alternate strategies that can be utilized.

This page intends to demonstrate some situations that are more complicated and suggest alternative approaches to solving them. This page may not cover every possible situation. The alternate approaches demonstrated on this page maybe suited for more than just the example situation each are paired with. But they should help think about alternate approaches when things get complicated.

Dynamic Values

Each Interaction supports dynamic values. This can be useful when things are deterministic. However, if the options for a Choice are retrieved from an external server, it can be hard to implement the conditional logic. In the following example, the data retrieval logic is encapsulated into a function that is called ahead of time. This allows the application to handle retrival errors or other validation before utilizing columbo to prompt the user for their selection. Additional, default can be set to a value that is known to exist in the options list, even without prior knowledge of the options.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import random

import columbo


def get_dog_breeds() -> list[str]:
    # In the real world this might actually be a GET request to an external server.
    possible_breeds = [
        "Basset Hound",
        "Great Dane",
        "Golden Retriever",
        "Poodle",
        "Dachshund",
    ]
    return random.choices(possible_breeds, k=random.randint(2, len(possible_breeds)))


all_dogs = get_dog_breeds()
interactions = [
    columbo.Choice(
        name="favorite",
        message="Which dog breed do you like best?",
        options=all_dogs,
        default=all_dogs[0],
    )
]
print(columbo.get_answers(interactions))

Optional Questions

Each Interaction can be optional. However, there are times where a number of those Interactions all rely on the same check to determine if the questions should be asked. One strategy to achieve this is to have same function could be passed to should_ask for each Interaction. An alternate strategy is to not limit the code to a single list of Interactions. get_answers() and parse_args() can be called multiple times within an application. Both functions can be passed the resultant Answers instance returned from the first call in order to keep the answers context moving forward.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import columbo

initial_user_answers = columbo.get_answers(
    [columbo.Confirm("has_dog", "Do you have a dog?", default=True)]
)
if initial_user_answers["has_dog"]:
    interactions = [
        columbo.Echo(
            "Because you have have a dog, we want to ask you some more questions.",
        ),
        columbo.BasicQuestion(
            "dog_name",
            "What is the name of the dog?",
            default="Kaylee",
        ),
        columbo.BasicQuestion(
            "dog_breed",
            "What is the breed of the dog?",
            default="Basset Hound",
        ),
    ]
    user_answers = columbo.get_answers(interactions, answers=initial_user_answers)
else:
    user_answers = initial_user_answers

print(user_answers)

Branching Paths

The fact that each Interaction can be optional can be used to support branching paths. However, for paths the diverge significantly, it can be hard to keep track of how the should_ask values interact. Similar to optional questions, a strategy to address this is to not limit the code to a single list of Interactions. get_answers() and parse_args() can be called multiple times within an application. This allows the application to manage the branching directly. Both functions can be passed the resultant Answers instance returned from the first call in order to keep the answers context moving forward.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import columbo


def outcome(answers: columbo.Answers) -> str:
    if answers.get("has_key", False):
        return "You try the the key on the lock. With a little jiggling, it finally opens. You open the gate and leave."
    if answers.get("has_hammer", False):
        return "You hit the lock with the hammer and it falls to the ground. You open the gate and leave."
    return (
        "Unable to open the gate yourself, you yell for help. A farmer in the nearby field hears you. "
        "He reaches into his pocket and pulls out a key to unlock the gate and open it. "
        "As you walk through the archway he says, "
        '"What I don\'t understand is how you got in there. This is the only key."'
    )


interactions = [
    columbo.Echo(
        "You wake up in a room that you do not recognize. "
        "In the dim light, you can see a large door to the left and a small door to the right."
    ),
    columbo.Choice(
        "which_door",
        "Which door do you walk through?",
        options=["left", "right"],
        default="left",
    ),
]
user_answers = columbo.get_answers(interactions)
if user_answers["which_door"] == "left":
    interactions = [
        columbo.Echo(
            "You step into a short hallway and the door closes behind you, refusing to open again. "
            "As you walk down the hallway, there is a small side table with a key on it.",
        ),
        columbo.Confirm(
            "has_key",
            "Do you pick up the key before going through the door at the other end?",
            default=True,
        ),
    ]
else:
    interactions = [
        columbo.Echo(
            "You step into smaller room and the door closes behind, refusing to open again. "
            "The room has a single door on the opposite side of the room and a work bench with a hammer on it.",
        ),
        columbo.Confirm(
            "has_hammer",
            "Do you pick up the hammer before going through the door at the other side?",
            default=True,
        ),
    ]

interactions.extend(
    [
        columbo.Echo(
            "You enter a small courtyard with high walls. There is an archway that would allow you to go free, "
            "but the gate is locked."
        ),
        columbo.Echo(outcome),
    ]
)

user_answers = columbo.get_answers(interactions, answers=user_answers)
print(user_answers)

Direct Interaction

get_answers() provides a helpful functionality for iterating over multiple Interactions and collecting the responses. However, it is implemented using methods that are directly available on each Interaction object. If an application wants full control over the flow of the user prompts, ask() and display() can be called as needed.