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