From 4cfdfcd463d609eaa2dab2e6d5e502b55d1d615b Mon Sep 17 00:00:00 2001
From: Patrick Lannigan
Date: Fri, 13 Oct 2023 11:29:20 -0400
Subject: [PATCH] Add documentation for alternate ways to structure
interactions
---
.bandit | 2 +-
docker/validate_docs.sh | 7 +-
docs/examples/alternate_branching_story.py | 66 +++++++++++++++++
docs/examples/alternate_dynamic_options.py | 27 +++++++
docs/examples/alternate_optional_questions.py | 26 +++++++
docs/usage-guide/advanced-usage.md | 74 +++++++++++++++++++
docs/usage-guide/fundamentals.md | 2 +
.../optional-questions-and-branching.md | 9 ++-
mkdocs.yml | 1 +
9 files changed, 209 insertions(+), 5 deletions(-)
create mode 100644 docs/examples/alternate_branching_story.py
create mode 100644 docs/examples/alternate_dynamic_options.py
create mode 100644 docs/examples/alternate_optional_questions.py
create mode 100644 docs/usage-guide/advanced-usage.md
diff --git a/.bandit b/.bandit
index 8b1406da..488615a4 100644
--- a/.bandit
+++ b/.bandit
@@ -1,2 +1,2 @@
[bandit]
-exclude: *venv*,*env*,*scratch*,./test
+exclude: *venv*,*env*,*scratch*,./test,./docs/examples/
diff --git a/docker/validate_docs.sh b/docker/validate_docs.sh
index db6e3725..95867895 100755
--- a/docker/validate_docs.sh
+++ b/docker/validate_docs.sh
@@ -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!";
diff --git a/docs/examples/alternate_branching_story.py b/docs/examples/alternate_branching_story.py
new file mode 100644
index 00000000..fa3267d1
--- /dev/null
+++ b/docs/examples/alternate_branching_story.py
@@ -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)
diff --git a/docs/examples/alternate_dynamic_options.py b/docs/examples/alternate_dynamic_options.py
new file mode 100644
index 00000000..34949534
--- /dev/null
+++ b/docs/examples/alternate_dynamic_options.py
@@ -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))
diff --git a/docs/examples/alternate_optional_questions.py b/docs/examples/alternate_optional_questions.py
new file mode 100644
index 00000000..c3d4e42b
--- /dev/null
+++ b/docs/examples/alternate_optional_questions.py
@@ -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)
diff --git a/docs/usage-guide/advanced-usage.md b/docs/usage-guide/advanced-usage.md
new file mode 100644
index 00000000..f0b3f526
--- /dev/null
+++ b/docs/usage-guide/advanced-usage.md
@@ -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
diff --git a/docs/usage-guide/fundamentals.md b/docs/usage-guide/fundamentals.md
index bdb8f770..79d2fb9e 100644
--- a/docs/usage-guide/fundamentals.md
+++ b/docs/usage-guide/fundamentals.md
@@ -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
diff --git a/docs/usage-guide/optional-questions-and-branching.md b/docs/usage-guide/optional-questions-and-branching.md
index 2689ca47..7157c6b2 100644
--- a/docs/usage-guide/optional-questions-and-branching.md
+++ b/docs/usage-guide/optional-questions-and-branching.md
@@ -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
\ No newline at end of file
+## 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
diff --git a/mkdocs.yml b/mkdocs.yml
index 23ebcb3d..45fb787b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -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