Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation for alternate ways to structure interactions #548

Merged
merged 1 commit into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bandit
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[bandit]
exclude: *venv*,*env*,*scratch*,./test
exclude: *venv*,*env*,*scratch*,./test,./docs/examples/
7 changes: 4 additions & 3 deletions docker/validate_docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
set -e

# install columbo
pip install . -q;
pip install . -q

for filename in docs/examples/*.py; do
printf "\n\n --- %s ---\n" "${filename}"
# provide default answers if example python file asks for input
yes "" | python ${filename};
yes "" | python "${filename}";
done

# this will only get printed if all examples finish succesfully
# this will only get printed if all examples finish successfully
printf "\n\n\nAll of the documentation examples can be run!";
66 changes: 66 additions & 0 deletions docs/examples/alternate_branching_story.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,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)
27 changes: 27 additions & 0 deletions docs/examples/alternate_dynamic_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,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))
26 changes: 26 additions & 0 deletions docs/examples/alternate_optional_questions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,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)
74 changes: 74 additions & 0 deletions docs/usage-guide/advanced-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Advanced Usage

The [Overview][overview] and [Getting Started][getting-started] pages show simplified examples
of how to use `columbo`. These examples have consisted of:

* statically defined list of `Interaction`s which are then passed to [get_answers()][get-answers] or
[parse_args()][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][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.

```python linenums="1"
{!examples/alternate_dynamic_options.py!}
```

## Optional Questions

Each `Interaction` can be [optional][optional]. However, there are times where a number of those `Interaction`s 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 `Interaction`s. [get_answers()][get-answers] and [parse_args()][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.

```python linenums="1"
{!examples/alternate_optional_questions.py!}
```

## Branching Paths

The fact that each `Interaction` can be [optional][optional] can be used to support [branching paths][branching].
However, for paths the diverge significantly, it can be hard to keep track of how the `should_ask` values interact.
Similar to [optional questions][optional-questions], a strategy to address this is to not limit the code to a single
list of `Interaction`s. [get_answers()][get-answers] and [parse_args()][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.


```python linenums="1"
{!examples/alternate_branching_story.py!}
```

## Direct Interaction

[get_answers()][get-answers] provides a helpful functionality for iterating over multiple `Interaction`s 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()][ask] and [display()][display] can be called as
needed.

[overview]: ../index.md
[getting-started]: ../getting-started.md
[get-answers]: ../api.md#columbo.get_answers
[parse-args]: ../api.md#columbo.parse_args
[dynamic-values]: ../getting-started.md#dynamic-values
[optional]: optional-questions-and-branching.md#optional-questions
[branching]: optional-questions-and-branching.md#branching-paths
[optional-questions]: #optional-questions
[ask]: ../api.md#columbo._interaction.BasicQuestion.ask
[display]: ../api.md#columbo._interaction.Echo.display
2 changes: 2 additions & 0 deletions docs/usage-guide/fundamentals.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ the constructor requires an argument to be a static value.
* [Optional Questions & Branching][optional-questions]
* [Validators][validators]
* [Command Line Interface][command-line]
* [Advanced Usage][advanced-usage]

[getting-started]: ../getting-started.md
[interactions]: interactions.md
[optional-questions]: optional-questions-and-branching.md
[validators]: validators.md
[command-line]: command-line.md
[advanced-usage]: advanced-usage.md
9 changes: 8 additions & 1 deletion docs/usage-guide/optional-questions-and-branching.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,11 @@ branching paths by supplying different `should_ask` values that will never both
The import thing to note in the example above is that the `Answers` dictionary can have a key-value pair for
`has_key` or `has_hammer`, not both.

[choose-your-own-adventure]: https://en.wikipedia.org/wiki/Choose_Your_Own_Adventure
## Complicated Situations

While `should_ask` is capable of supporting complex combinations of optional questions and branching paths,
there are times where only using that functionality can make the code harder to read and understand. There
are [alternate strategies][advanced-usage] that can be used in order to make the code easier to follow.

[choose-your-own-adventure]: https://en.wikipedia.org/wiki/Choose_Your_Own_Adventure
[advanced-usage]: advanced-usage.md
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ nav:
- Optional Questions & Branching: usage-guide/optional-questions-and-branching.md
- Validators: usage-guide/validators.md
- Command Line Interface: usage-guide/command-line.md
- Advanced Usage: usage-guide/advanced-usage.md
- Why Columbo?: why-columbo.md
- Reference: api.md
- Development Guide: development-guide.md
Expand Down