├── docs ├── code_reference │ ├── cli.md │ ├── quiz.md │ ├── layout.md │ ├── utils.md │ ├── examples.md │ ├── session.md │ └── index.md ├── assets │ ├── logo.png │ └── demo-getting-help.svg ├── user_guide │ ├── index.md │ └── cli.md ├── learning_guide │ ├── index.md │ ├── quiz.md │ ├── publishing.md │ ├── cli.md │ ├── testing.md │ └── documentation.md ├── demos.md ├── index.md └── notebooks │ └── quizli.ipynb ├── quizli ├── __init__.py ├── utils.py ├── session.py ├── main.py ├── examples.py ├── quiz.py └── layout.py ├── examples └── quiz.csv ├── .coveragerc ├── .github └── workflows │ ├── deploy_docs.yml │ └── test_package.yml ├── pyproject.toml ├── tests └── test_quizli.py ├── .gitignore ├── README.md └── mkdocs.yml /docs/code_reference/cli.md: -------------------------------------------------------------------------------- 1 | ::: quizli.main 2 | -------------------------------------------------------------------------------- /docs/code_reference/quiz.md: -------------------------------------------------------------------------------- 1 | ::: quizli.quiz 2 | -------------------------------------------------------------------------------- /quizli/__init__.py: -------------------------------------------------------------------------------- 1 | from quizli.quiz import * 2 | -------------------------------------------------------------------------------- /docs/code_reference/layout.md: -------------------------------------------------------------------------------- 1 | ::: quizli.layout 2 | -------------------------------------------------------------------------------- /docs/code_reference/utils.md: -------------------------------------------------------------------------------- 1 | ::: quizli.utils 2 | -------------------------------------------------------------------------------- /docs/code_reference/examples.md: -------------------------------------------------------------------------------- 1 | ::: quizli.examples 2 | -------------------------------------------------------------------------------- /docs/code_reference/session.md: -------------------------------------------------------------------------------- 1 | ::: quizli.session 2 | 3 | -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwenker/quizli/HEAD/docs/assets/logo.png -------------------------------------------------------------------------------- /examples/quiz.csv: -------------------------------------------------------------------------------- 1 | How to install [blue]quizli[/blue]?,pip install quizli 2 | What is the opposite of 'no'?,yes 3 | What is 1+1?,2 4 | Which comedy group gave python it's name?,monty python 5 | What is the meaning of life?,42 6 | Do you like this quiz?,yes 7 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = quizli 4 | 5 | [report] 6 | exclude_lines = 7 | if self.debug: 8 | pragma: no cover 9 | raise NotImplementedError 10 | if __name__ == .__main__.: 11 | ignore_errors = True 12 | omit = 13 | tests/* 14 | -------------------------------------------------------------------------------- /docs/user_guide/index.md: -------------------------------------------------------------------------------- 1 | # :rocket: User Guide 2 | 3 | Welcome to `quizli's` user guide! 4 | 5 | Here you'll find usage and reference material for `quizli's`... 6 | 7 | 1. Command Line Interface 8 | 2. Source Code 9 | 10 | !!! tip 11 | To see `quizli` in action, take a look at the [demos page](../demos.md). 12 | -------------------------------------------------------------------------------- /.github/workflows/deploy_docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-python@v2 13 | with: 14 | python-version: 3.x 15 | - run: pip install rich typer-cli mkdocs-material mkdocstrings mkdocs-jupyter mkdocs-git-revision-date-localized-plugin ipython_genutils 16 | - run: mkdocs gh-deploy --force 17 | -------------------------------------------------------------------------------- /docs/code_reference/index.md: -------------------------------------------------------------------------------- 1 | # Code Reference 2 | 3 | :rocket: The code reference in here is automatically generated from the source code (using `mkdocstrings`), and organized by modules: 4 | 5 | - Quiz (`quizli.quiz`) 6 | - Layout (`quizli.layout`) 7 | - Session(`quizli.session`) 8 | - examples(`quizli.examples`) 9 | - CLI (`quizli.main`) 10 | 11 | !!! tip 12 | Take a look at the [`documentation` section](../learning_guide/documentation.md) in `quizli's` [learning guide](../learning_guide/index.md) to understand how we automate the generation of the documentation. 13 | -------------------------------------------------------------------------------- /docs/learning_guide/index.md: -------------------------------------------------------------------------------- 1 | # :mortar_board: Welcome to the Learning Guide 2 | 3 | This learning guide teaches you how to create your own open-source Python project 4 | using `quizli` as an example. 5 | 6 | | | Learning Guide | 7 | | --- | --- | 8 | Interactive quiz app| [How to create an interactive Python quiz app?](quiz.md) 9 | Command Line Interface | [How to add a CLI to your quiz app?](cli.md) 10 | Documentation | [How to create a slick documentation for your app?](documentation.md) 11 | Publishing | [How to build, manage and publish your Python package to PyPi?](publishing.md) 12 | Testing | [How to test your app?](testing.md) 13 | 14 | !!! Info "Information" 15 | 16 | !!! abstract "Learning Objectives" 17 | 18 | At the beginning of each section of the learning guide you will find **learning objectives**. 19 | 20 | The goal is to... 21 | 22 | * Give you an idea about the main points of the sections prior to reading them 23 | * Help you test your understanding about the topic after reading it 24 | -------------------------------------------------------------------------------- /docs/demos.md: -------------------------------------------------------------------------------- 1 | # How to use `quizli`? 2 | 3 | !!! info 4 | The demos are created with the [`termtosvg`](https://github.com/nbedos/termtosvg) tool using the `window_frame_powershell` theme. 5 | 6 | ## How to get help ? 7 | 8 | !!! example 9 | - `quizli --help` 10 | - `quizli demo --help` 11 | - `quizli start --help` 12 | 13 | ![Demo](assets/demo-getting-help.svg) 14 | 15 | ## How to start a quiz from a csv file? 16 | 17 | !!! example 18 | - `quizli start --from-csv examples/quiz.csv --in-order` 19 | 20 | !!! tip 21 | We use `--in-order` to _not_ shuffle the quiz before starting it. 22 | 23 | ![Demo](assets/demo-from-csv.svg) 24 | 25 | ## How to start a built-in quiz? 26 | 27 | ### Python Quiz 28 | 29 | !!! example 30 | - `quizli start --quiz-name python-quiz --randomize` 31 | 32 | !!! tip 33 | We use `--randomize` to explicitly shuffle the quiz before starting it. 34 | 35 | #### Quiz on Python modules 36 | 37 | ![Demo](assets/demo-python-quiz-modules.svg) 38 | 39 | #### Quiz on Python objects 40 | 41 | ![Demo](assets/demo-python-quiz-objects.svg) 42 | 43 | ### Binary Number Quiz 44 | 45 | !!! example 46 | - `quizli start --quiz-name binary_number_quiz --mode sudden_death` 47 | 48 | !!! tip 49 | We use `--mode sudden_death` to configure the quiz session such that a single incorrect answer terminates the quiz. 50 | 51 | ![Demo](assets/demo-binary-number-quiz.svg) 52 | 53 | ## How to open this demo page from the CLI ? 54 | 55 | !!! example 56 | - `quizli demo` 57 | 58 | 59 | -------------------------------------------------------------------------------- /quizli/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains some utility functions 3 | """ 4 | 5 | import random 6 | from typing import Union 7 | 8 | __all__ = ["em", "motivate"] 9 | 10 | 11 | def em(message: Union[str, int, float], col: str = "blue") -> str: 12 | """Emphasize a message string with markup 13 | 14 | Args: 15 | message (Union[str, int, float]): The message string 16 | col (str): The color of the message 17 | 18 | Returns: 19 | message in markup 20 | """ 21 | 22 | return f"[{col} bold]{message}[/{col} bold]" 23 | 24 | 25 | def motivate() -> str: 26 | """Return random motivational text & emoji 27 | 28 | This function randomly choses a motivational text along with a motivational emoji 29 | to be shown in the quiz layout whenever the user answers correctly. 30 | 31 | Returns: 32 | str: The motivational text + emoji 33 | """ 34 | motivational_texts = [ 35 | "So good...keep going!", 36 | "You are amazing!", 37 | "How do you do that?", 38 | "Just....unbelievable!", 39 | "Do you quiz professionally?", 40 | "You are unstoppable!", 41 | "You are on fire!", 42 | "Legen....wait for it....dary!", 43 | "Alright, I need to ask harder questions...", 44 | "Perfect!", 45 | "You are unbeatable", 46 | "And now for something completely different...", 47 | ] 48 | 49 | motivational_emojis = [ 50 | ":exploding_head:" ":fire:", 51 | ":rocket:", 52 | ":exclamation_mark:", 53 | ":brain:", 54 | ":direct_hit:", 55 | ":flexed_biceps:", 56 | ":mechanical_arm:", 57 | ":party_popper:", 58 | ":partying_face:", 59 | ":rainbow:", 60 | ":sparkles:", 61 | ":sunglasses:", 62 | ] 63 | return f"{random.choice(motivational_texts)} {random.choice(motivational_emojis)}" 64 | -------------------------------------------------------------------------------- /docs/user_guide/cli.md: -------------------------------------------------------------------------------- 1 | # `quizli` 2 | 3 | Welcome to quizli 4 | 5 | An educational project teaching how to open-source an interactive Python quiz app 6 | 7 | Check out the project at: https://github.com/pwenker/quizli/ 8 | 9 | **Usage**: 10 | 11 | ```console 12 | $ quizli [OPTIONS] COMMAND [ARGS]... 13 | ``` 14 | 15 | **Options**: 16 | 17 | * `--install-completion`: Install completion for the current shell. 18 | * `--show-completion`: Show completion for the current shell, to copy it or customize the installation. 19 | * `--help`: Show this message and exit. 20 | 21 | **Commands**: 22 | 23 | * `demo`: Open the documentation page in the browser 24 | * `start`: Start the quiz with the configuration of your... 25 | 26 | ## `quizli demo` 27 | 28 | Open the documentation page in the browser 29 | 30 | **Usage**: 31 | 32 | ```console 33 | $ quizli demo [OPTIONS] 34 | ``` 35 | 36 | **Options**: 37 | 38 | * `--help`: Show this message and exit. 39 | 40 | ## `quizli start` 41 | 42 | Start the quiz with the configuration of your choice. 43 | 44 | ### Select Quiz Content 45 | 46 | There are 2 ways to create a quiz with `quizli`: 47 | 48 | 1. From a csv file containing question-answer pairs: e.g. `quizli start --from-csv examples/quiz.csv` 49 | 2. From a function in the `example` module: e.g. `quizli start --quiz-name python_quiz` 50 | 51 | ### Select Quiz Settings 52 | 53 | You can choose to 54 | 55 | - Shuffle the quiz with the `--randomize` flag (default), or keep it `--in-order`. 56 | 57 | - Terminate the quiz with the first wrong answer with `mode=sudden_death`, or continue on failure with `mode=complete` (default). 58 | 59 | **Usage**: 60 | 61 | ```console 62 | $ quizli start [OPTIONS] 63 | ``` 64 | 65 | **Options**: 66 | 67 | * `--from-csv PATH`: Read a quiz from a csv-file 68 | * `--mode [sudden_death|complete]`: Select the condition for the quiz to end [default: complete] 69 | * `--quiz-name [python_quiz|binary_number_quiz]`: Select a built-in quiz [default: python_quiz] 70 | * `--randomize / --in-order`: Shuffle the quiz before starting it [default: True] 71 | * `--help`: Show this message and exit. 72 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | # {==(1) Annotating our package==} 3 | # First we define some basic descriptions about our project. 4 | # More information about valid sections can be found in this link: 5 | # https://python-poetry.org/docs/master/pyproject/ 6 | [tool.poetry] 7 | name = "quizli" 8 | homepage = "https://github.com/pwenker/quizli" 9 | documentation = "https://pwenker.github.io/quizli" 10 | version = "1.0.0" 11 | description = "An educational project teaching how to open-source an interactive Python quiz app" 12 | authors = ["Pascal Wenker "] 13 | readme = "README.md" 14 | license = "MIT" 15 | classifiers = [ # https://pypi.org/classifiers/ 16 | "Programming Language :: Python :: 3 :: Only", 17 | "Environment :: Console", 18 | "Intended Audience :: Education", 19 | "Intended Audience :: Developers", 20 | "Natural Language :: English", 21 | "Topic :: Documentation", 22 | "Topic :: Education", 23 | "Topic :: Software Development", 24 | "Typing :: Typed", 25 | ] 26 | 27 | # {==(2) Defining an entrypoint for our CLI==} 28 | # Here we define the entrypoint to our app. Since we want to let 29 | # users run the app with the `quizli` command, we define it as such 30 | # below 31 | [tool.poetry.scripts] 32 | quizli = "quizli.main:app" 33 | 34 | # {==(3) Declaring Dependencies==} 35 | # The following section defines the dependencies of our project. 36 | # Note that poetry lets us split them up in 37 | # - regular dependencies, and 38 | # - dev-dependencies. 39 | # In this way, a users can install it with `poetry install --no-dev` 40 | # to not install the development dependencies they don't need. 41 | [tool.poetry.dependencies] 42 | python = "^3.8" 43 | rich = "^11.0.0" 44 | typer-cli = "^0.0.12" 45 | 46 | [tool.poetry.dev-dependencies] 47 | # Testing 48 | pytest = "^7.0.1" 49 | pytest-cov = "^3.0.0" 50 | typing-extensions = "^4.1.1" 51 | # Documentation 52 | mkdocs-material = "^8.1.8" 53 | ## Plugins 54 | mkdocstrings = "^0.17.0" 55 | mkdocs-jupyter = "^0.19.0" 56 | mkdocs-git-revision-date-localized-plugin = "^1.0.0" 57 | 58 | # {==(4) Setting the build-sytem==} 59 | # Here, we declare that we want poetry to build our package. 60 | [build-system] # See: https://www.python.org/dev/peps/pep-0518/#id27 61 | requires = ["poetry-core>=1.0.0"] # For more information see: PEP 518 62 | build-backend = "poetry.core.masonry.api" # For more information see: PEP 517 63 | -------------------------------------------------------------------------------- /quizli/session.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains all the necessary logic to create quiz sessions 3 | """ 4 | 5 | 6 | from dataclasses import dataclass 7 | 8 | from rich import print 9 | from rich.prompt import Confirm 10 | 11 | from quizli import Quiz, QuizConfig, QuizMode 12 | from quizli.layout import FancyQuizLayout 13 | 14 | __all__ = ["QuizSession"] 15 | 16 | 17 | @dataclass 18 | class QuizSession: 19 | """This class makes a quiz interactive 20 | 21 | It accepts a `quiz` (`Quiz`) and interactively renders it within a `layout` (`QuizLayout`) 22 | to interactively ask the user questions according to a `config` (`QuizConfig`). 23 | 24 | 25 | Attributes: 26 | quiz: Determines the quiz to be started 27 | config: Determines the quiz settings 28 | layout: Determines how the quiz will look like 29 | """ 30 | 31 | quiz: Quiz 32 | config: QuizConfig 33 | layout: FancyQuizLayout = FancyQuizLayout() 34 | 35 | def _initialize(self) -> None: 36 | """Apply some of the quiz settings and initialize the quiz layout""" 37 | if self.config.randomize: 38 | self.quiz.shuffle() 39 | 40 | self.layout.initialize(quiz_name=self.quiz.name, n_questions=len(self.quiz)) 41 | 42 | def start(self) -> None: 43 | """Start the quiz 44 | 45 | This method powers the whole interactivity of the quiz, managing 46 | layout updates, as well as iterating through the quiz to ask questions. 47 | """ 48 | self._initialize() 49 | 50 | print(self.layout.layout) 51 | 52 | for i, item in enumerate(self.quiz.items, start=1): 53 | 54 | self.layout.show_question(item=item, idx=i) 55 | 56 | answer_correct = item.ask(show_question=False) 57 | 58 | self.layout.reveal_answer(item, answer_correct=answer_correct) 59 | 60 | if not answer_correct and self.config.mode == QuizMode.SUDDEN_DEATH: 61 | self.start() if Confirm.ask( 62 | "Retry the quiz?" 63 | ) else self.layout.show_results(has_won=False) 64 | return 65 | else: 66 | if not Confirm.ask("Continue", default="y"): 67 | self.start() if Confirm.ask( 68 | "Restart the quiz?" 69 | ) else self.layout.show_results() 70 | return 71 | 72 | if self.config.mode == QuizMode.SUDDEN_DEATH: 73 | self.layout.show_results(has_won=True) 74 | else: 75 | self.layout.show_results() 76 | -------------------------------------------------------------------------------- /tests/test_quizli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic testing for the `quizli` app. 3 | 4 | See: https://typer.tiangolo.com/tutorial/testing/ 5 | """ 6 | 7 | 8 | import pytest 9 | from typer.testing import CliRunner 10 | 11 | from quizli.main import app 12 | 13 | runner = CliRunner() 14 | 15 | 16 | # ╭──────────────────────────────────────────────────────────╮ 17 | # │ Test Help Commands │ 18 | # ╰──────────────────────────────────────────────────────────╯ 19 | help_commands = [ 20 | "--help", 21 | "demo --help", 22 | "start --help", 23 | ] 24 | 25 | 26 | @pytest.mark.parametrize("test_input", help_commands) 27 | def test_help_commands( 28 | test_input, 29 | ): 30 | result = runner.invoke(app, test_input) 31 | assert result.exit_code == 0 32 | 33 | 34 | # ╭──────────────────────────────────────────────────────────╮ 35 | # │ Test Demo Command │ 36 | # ╰──────────────────────────────────────────────────────────╯ 37 | def test_demo(): 38 | result = runner.invoke(app, "demo") 39 | assert result.exit_code == 0 40 | 41 | 42 | # ╭──────────────────────────────────────────────────────────╮ 43 | # │ Test Start Command │ 44 | # ╰──────────────────────────────────────────────────────────╯ 45 | @pytest.mark.parametrize( 46 | "test_cmd, test_inp, exp_out", 47 | [ 48 | ( 49 | "start --quiz-name python_quiz", 50 | ["modules\n", "Answer\n", "n\n", "n\n"], 51 | "See you soon", 52 | ), 53 | ( 54 | "start --quiz-name python_quiz", 55 | ["objects\n", "quizli\n", "Answer\n", "n\n", "n\n"], 56 | "See you soon", 57 | ), 58 | ], 59 | ) 60 | def test_start_builtin_quiz(test_cmd, test_inp, exp_out): 61 | result = runner.invoke(app, test_cmd, input="".join(test_inp)) 62 | 63 | assert result.exit_code == 0 64 | assert exp_out in result.stdout 65 | 66 | 67 | @pytest.mark.parametrize( 68 | "test_cmd, test_inp, exp_out", 69 | [ 70 | ( 71 | "start --from-csv examples/quiz.csv --mode sudden_death --in-order", 72 | ["pip install quizli", "yes", "2", "monty python", "42", "yes", ""], 73 | "🏆 You won! 🏆 See you soon, champion!", 74 | ), 75 | ( 76 | "start --from-csv examples/quiz.csv --mode sudden_death --in-order", 77 | ["pip install quizli", "yes", "2", "monty python", "42", "no", "n"], 78 | "Game Over! 😞 See you soon !", 79 | ), 80 | ], 81 | ) 82 | def test_start_quiz_from_csv(test_cmd, test_inp, exp_out): 83 | result = runner.invoke(app, test_cmd, input="\n\n".join(test_inp)) 84 | 85 | assert result.exit_code == 0 86 | assert exp_out in result.stdout 87 | -------------------------------------------------------------------------------- /docs/learning_guide/quiz.md: -------------------------------------------------------------------------------- 1 | # How to create an interactive quiz app with Python? 2 | 3 | Since the main purpose of the learning guide is to teach you how to create an open source project for an _existing_ app or library, we will only shortly describe `quizli` itself. 4 | 5 | !!! abstract "Learning Objectives" 6 | By the end of this section, you should be able to: 7 | 8 | * List the main building blocks of the `quizli` 9 | * Modify & improve `quizli` via the experience you gained through the assignments 10 | * Work with the `rich` library to make your own app interactive and beautiful 11 | 12 | ## 1. Main building blocks of a simple quiz app 13 | !!! note "Assignment 1" 14 | 15 | - a) Read through the code reference of the [`quiz` module](../code_reference/quiz.md) 16 | - b) Take a look at the exemplary jupyter notebook about [creating a simple quiz](../notebooks/quizli.ipynb) 17 | 18 | !!! info "Resources" 19 | 20 | - Dataclasses: [PEP-557](https://www.python.org/dev/peps/pep-0557/) & [Section in Python Docs](https://docs.python.org/3/library/dataclasses.html) 21 | 22 | ## 2. Make your quiz beautiful and interactive 23 | 24 | In order to bring color and interactivity into our quiz we make heavy use of the [`rich`](https://github.com/Textualize/rich) library. 25 | 26 | !!! note "Assignment 2" 27 | - a) Read through the code reference of the [`layout` module](../code_reference/layout.md) and the [`session` module](../code_reference/session.md) 28 | - b) :material-powershell: Create your own quiz layout class that inherits from `QuizLayoutBase`, similar to the `FancyQuizLayout`. It could be a minimalist quiz layout, or maybe you want to show off your creative power and design a beautiful und super-complex layout. 29 | 30 | !!! info "Resources" 31 | - Read through the following sections of the `rich` documentation: 32 | - [Introduction](https://rich.readthedocs.io/en/latest/introduction.html) 33 | - [Layouts](https://rich.readthedocs.io/en/latest/layout.html) 34 | - [Prompts](https://rich.readthedocs.io/en/latest/prompt.html) 35 | 36 | ## 2. Dynamically create a quiz 37 | 38 | The `examples` module shows how to dynamically create a quiz and have it automatically pop up in quizli's CLI. 39 | 40 | !!! note "Assignment 3" 41 | - a) Read through the code reference of the [`example` module](../code_reference/examples.md) 42 | - b) :material-powershell: Create your own dynamic quiz, by implementing a function similar to the `python_quiz` and the `binary_number_quiz` functions. Add your quiz as a variant to the `QuizKind` enumeration, so that it will be automatically added as an option in quizli's CLI. 43 | - c) :material-powershell: The built-in Python quiz has a problem: when asking for the name of a Python object/module given it's description, the description sometimes contains the answer itself. Improve the `python_quiz` by writing code that strips out the answer from the question field before showing it. 44 | -------------------------------------------------------------------------------- /docs/learning_guide/publishing.md: -------------------------------------------------------------------------------- 1 | # Managing and Publishing with [**Poetry**](https://python-poetry.org/) 2 | 3 | !!! abstract "Learning Objectives" 4 | By the end of this section, you should be able to: 5 | 6 | * Explain _what_ Poetry is. 7 | * Explain _why_ Poetry is a great choice for dependency management and packaging in Python. 8 | * Explain what a `pyproject.toml` file is and how it is useful. 9 | * Publish your own package with Poetry 10 | 11 | ## What is **Poetry**? 12 | 13 | ???+ question "What is Poetry?" 14 | 15 | !!! quote 16 | Poetry is a tool for **dependency management** and **packaging** in Python. It allows you to declare the libraries your project depends on and it will manage (install/update) them for you. 17 | 18 | !!! example "Basic poetry usage/workflow" 19 | Read poetry's [basic usage guide](https://python-poetry.org/docs/master/basic-usage/) 20 | 21 | 22 | ## What's that `pyproject.toml` file? 23 | 24 | We answer the following questions by referring to the relevant sections in **PEP 518**. 25 | This saves us some time, avoids reproducing existing information, and you end up with some experience reading through PEP's :). 26 | 27 | !!! question "What was the rationale behind PEP518 and the `pyproject.toml` file? :material-arrow-right: [Answer](https://www.python.org/dev/peps/pep-0518/#rationale)" 28 | !!! question "Why was the TOML format chosen? :material-arrow-right: [Answer](https://www.python.org/dev/peps/pep-0518/#file-format)" 29 | !!! question "Why where YAML, JSON, and the configparser rejected? :material-arrow-right: [Answer](https://www.python.org/dev/peps/pep-0518/#json)" 30 | 31 | To see the `pyproject.toml` file in action, take a look at the following example. 32 | 33 | ### Example: `quizli's` `pyproject.toml` file 34 | !!! example 35 | ``` py title="pyproject.toml" 36 | 37 | --8<-- "pyproject.toml" 38 | ``` 39 | 40 | !!! tip "Poetry's new dependency groups" 41 | With version 1.2.0 poetry allows you to organize your dependencies by groups. 42 | 43 | For more information about this **dependency groups** feature, see 44 | the [Managing dependencies section](https://python-poetry.org/docs/master/managing-dependencies/) 45 | in the docs. 46 | 47 | ## Publishing the package 48 | 49 | !!! info 50 | In order to publish the package to PyPi you need to create an account at [https://pypi.org/](https://pypi.org/). 51 | 52 | To build & publish the package, we can again use Poetry: 53 | 54 | 1. Build the package: `poetry build` 55 | 2. Publish it: `poetry publish` 56 | 57 | That's all, the package is now available at [https://pypi.org/project/quizli/](https://pypi.org/project/quizli/) and 58 | can be installed with `pip install quizli`. 59 | 60 | 61 | 62 | 63 | !!! info "Resources" 64 | - [PEP 517 - A build-system independent format for source trees](https://www.python.org/dev/peps/pep-0517/) 65 | - [PEP 518 - Specifying Minimum Build System Requirements for Python Projects](https://www.python.org/dev/peps/pep-0518/) 66 | 67 | 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | -------------------------------------------------------------------------------- /docs/learning_guide/cli.md: -------------------------------------------------------------------------------- 1 | # How to create a CLI with [**Typer**](https://github.com/tiangolo/typer)? 2 | 3 | !!! abstract "Learning Objectives" 4 | By the end of this section, you should be able to: 5 | 6 | * Explain why **Typer** is an optimal choice to build your CLI 7 | * Navigate the official documentation of **Typer** 8 | * Build your own CLI from scratch 9 | * Automatically generate documentation for your CLI 10 | * Explain how `quizli's` CLI was built 11 | 12 | ## What is **Typer** and why should I use it? 13 | 14 | !!! quote 15 | **Typer** is a library for building CLI applications that users will love using and developers will love creating. Based on Python 3.6+ type hints. 16 | 17 | Maybe you have heard of or even used [**Click**](https://click.palletsprojects.com/en/8.0.x/) before - a popular Python package for creating command line interfaces in Python. 18 | Since **Typer** is based on Click, by using it you get all of Clicks's features, plus everything Typer adds on top of it. 19 | 20 | For me the most striking features/advantages of Typer are that it: 21 | 22 | - Utilizes [Python's type hints](https://www.python.org/dev/peps/pep-0484/) 23 | - Provides automatic help, and automatic completion (for most/all shells) 24 | - Allow to [generate Markdown documentation](https://typer.tiangolo.com/typer-cli/#generate-docs-with-typer-cli) for your CLI's build with Typer (see below) 25 | - Has sensible defaults (allows to write less code) 26 | - Is easy to use both for developers _and_ users 27 | - Contains a comprehensive documentation with lot's of examples and detailed tutorials 28 | 29 | There is also a [particular section in Typer's docs](https://typer.tiangolo.com/alternatives/) on how it compares to alternative libraries. 30 | 31 | !!! info "Recommended Reading" 32 | - [Typer's Intro](https://typer.tiangolo.com/) 33 | - [Typer's User Tutorial](https://typer.tiangolo.com/tutorial/) 34 | 35 | ## Using **Typer** for `quizli` 36 | 37 | ### Creating a CLI for `quizli` 38 | 39 | Below you see the source code of `quizli's` CLI. We added comments, prepended with (1) to (4) and highlighted in yellow, to explain 40 | how the source code relates to the resulting CLI app. 41 | 42 | 43 | ``` py title="quizli's CLI" 44 | 45 | --8<-- "quizli/main.py" 46 | ``` 47 | 48 | 49 | ### Generating Markdown documentation 50 | 51 | 52 | Take a look at the [CLI reference page](../user_guide/cli.md) in `quizli's` user guide section. 53 | 54 | What you see there was actually automatically generated with **Typer**, using a single, simple command: 55 | 56 | ``` 57 | typer quizli/main.py utils docs --name quizli --output docs/user_guide/cli.md 58 | ``` 59 | 60 | This feature allows you to write all the documentation of your CLI directly within the source file(s) simply using Python docstrings. 61 | This way, following the [DRY principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)[^1], it is easy to keep your docs up-to-date and 62 | in-sync with the source code. 63 | 64 | [^1]: The DRY principle is stated as "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system" 65 | 66 | ## Final remarks 67 | 68 | With our example we only scratch the surface of what is possible with **Typer**, and what is needed for a CLI 69 | to be useful in a real-world scenario. But it should get you going! :) 70 | 71 | !!! hint "A note about Best Practices" 72 | As soon as you are building a larger "real-world" CLI, it pays of for you and your users to adhere to some _best practices_: 73 | 74 | A comprehensive and elucidating guideline can be found here: [https://clig.dev/](https://clig.dev/). 75 | -------------------------------------------------------------------------------- /.github/workflows/test_package.yml: -------------------------------------------------------------------------------- 1 | name: test_package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | - Add_testing 9 | jobs: 10 | linting: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # ╭──────────────────────────────────────────────────────────╮ 14 | # │ Check-out repo and set-up python │ 15 | # ╰──────────────────────────────────────────────────────────╯ 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-python@v2 18 | # ╭──────────────────────────────────────────────────────────╮ 19 | # │ Load pip cache if cache exists │ 20 | # ╰──────────────────────────────────────────────────────────╯ 21 | - uses: actions/cache@v2 22 | with: 23 | path: ~/.cache/pip 24 | key: ${{ runner.os }}-pip 25 | restore-keys: ${{ runner.os }}-pip 26 | # ╭──────────────────────────────────────────────────────────╮ 27 | # │ Install and run linters │ 28 | # ╰──────────────────────────────────────────────────────────╯ 29 | - run: python -m pip install black isort 30 | - run: | 31 | black . --check 32 | isort . 33 | test: 34 | strategy: 35 | matrix: 36 | # NOTE: In the real world you should test on other 37 | # operating systems as well! 38 | os: [ "ubuntu-latest"] 39 | python-version: ["3.7", "3.8", "3.9", "3.10" ] 40 | defaults: 41 | run: 42 | shell: bash 43 | runs-on: ${{ matrix.os }} 44 | steps: 45 | # ╭──────────────────────────────────────────────────────────╮ 46 | # │ Check-out repo and set-up python │ 47 | # ╰──────────────────────────────────────────────────────────╯ 48 | - name: Check out repository 49 | uses: actions/checkout@v2 50 | - name: Set up python ${{ matrix.python-version }} 51 | id: setup-python 52 | uses: actions/setup-python@v2 53 | with: 54 | python-version: ${{ matrix.python-version }} 55 | # ╭──────────────────────────────────────────────────────────╮ 56 | # │ Install & configure poetry │ 57 | # ╰──────────────────────────────────────────────────────────╯ 58 | - name: Install Poetry 59 | uses: snok/install-poetry@v1 60 | with: 61 | virtualenvs-create: false 62 | # ╭──────────────────────────────────────────────────────────╮ 63 | # │ Install quizli │ 64 | # ╰──────────────────────────────────────────────────────────╯ 65 | - name: Install quizli 66 | run: poetry install --no-interaction 67 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 68 | # ╭──────────────────────────────────────────────────────────╮ 69 | # │ Run test suite and output coverage file │ 70 | # ╰──────────────────────────────────────────────────────────╯ 71 | - name: Run tests 72 | run: poetry run pytest --doctest-modules -vvv --cov-report=xml --cov=. 73 | # ╭──────────────────────────────────────────────────────────╮ 74 | # │ Upload coverage stats │ 75 | # ╰──────────────────────────────────────────────────────────╯ 76 | - name: Upload coverage 77 | uses: codecov/codecov-action@v1 78 | 79 | # ╭──────────────────────────────────────────────────────────╮ 80 | # │ NOTE: In a real-world example you would add a another │ 81 | # │ stage that runs if the test stage has been successful │ 82 | # │ and publishes the package │ 83 | # ╰──────────────────────────────────────────────────────────╯ 84 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://github.com/pwenker/quizli/blob/main/docs/assets/logo.png?raw=true) 2 | 3 | _An educational project teaching how to open-source an interactive Python quiz app_ 4 | 5 | | | quizli | 6 | | --- | --- | 7 | | Project Stats | [![GitHub Repo stars](https://img.shields.io/github/stars/pwenker/quizli?style=social)](https://github.com/pwenker/quizli) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/pwenker/quizli) ![Lines of code](https://img.shields.io/tokei/lines/github/pwenker/quizli) 8 | | Documentation | [![User Guide](https://img.shields.io/badge/docs-User%20Guide-brightgreen)](https://pwenker.github.io/quizli/user_guide) [![Learning Guide](https://img.shields.io/badge/docs-Learning%20Guide-brightgreen)](https://pwenker.github.io/quizli/learning_guide/) [![Demos](https://img.shields.io/badge/docs-Showcase-brightgreen)](https://pwenker.github.io/quizli/demos.html) | 9 | | Build status | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/pwenker/quizli/test_package) ![Codecov](https://img.shields.io/codecov/c/github/pwenker/quizli) | 10 | | Activity & Issue Tracking | ![GitHub last commit](https://img.shields.io/github/last-commit/pwenker/quizli) [![GitHub issues](https://img.shields.io/github/issues-raw/pwenker/quizli)](https://github.com/pwenker/quizli/issues?q=is%3Aopen+is%3Aissue) [![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/pwenker/quizli)](https://github.com/pwenker/quizli/issues?q=is%3Aissue+is%3Aclosed) | 11 | | PyPI | [![PyPI](https://img.shields.io/pypi/v/quizli)](https://pypi.org/project/quizli/) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/quizli) [![Downloads](https://pepy.tech/badge/quizli/month)](https://pepy.tech/project/quizli)| 12 | | News & Updates | [![Twitter Follow](https://img.shields.io/twitter/follow/PascalWenker?style=social)](https://twitter.com/PascalWenker) [![GitHub followers](https://img.shields.io/github/followers/pwenker?style=social)](https://github.com/pwenker)| 13 | 14 | ## Demo 15 | 16 | !!! hint "Check the [demo page](demos.md) for more examples on how to use `quizli`" 17 | 18 | 19 | 20 | ## :mortar_board: Learning Guide 21 | 22 | This guide teaches you how to effectively share a Python app with the open-source community. 23 | 24 | | | Learning Guide | 25 | | --- | --- | 26 | Interactive quiz app| [How to create an interactive Python quiz app?](learning_guide/quiz.md) 27 | Command Line Interface | [How to add a CLI to your quiz app?](learning_guide/cli.md) 28 | Documentation | [How to create a slick documentation for your app?](learning_guide/documentation.md) 29 | Publishing | [How to build, manage and publish your Python package to PyPi?](learning_guide/publishing.md) 30 | Testing | [How to test your app with a CI/CD pipeline?](learning_guide/testing.md) 31 | 32 | ## :rocket: User Guide 33 | 34 | This guide contains usage and reference material for the `quizli` app. 35 | 36 | | | User Guide | 37 | | --- | --- | 38 | CLI Reference | [Usage & reference for `quizli's` CLI](user_guide/cli.md) 39 | Code Reference | [Usage & reference for `quizli's` source code](code_reference/index.md) 40 | 41 | 42 | ## Quickstart 43 | 44 | ### :package: Installation 45 | 46 | Install quizli with [`pip`](https://pip.pypa.io/en/stable/getting-started/): 47 | 48 | ```console 49 | pip install quizli 50 | ``` 51 | 52 | ### :zap: Entrypoint 53 | To get help about `quizli's` commands open your console and type: 54 | 55 | ```console 56 | quizli --help 57 | ``` 58 | 59 | The same works for subcommands, e.g. : 60 | 61 | ```console 62 | quizli demos --help 63 | ``` 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://github.com/pwenker/quizli/blob/main/docs/assets/logo.png?raw=true) 2 | 3 | _An educational project teaching how to open-source an interactive Python quiz app_ 4 | 5 | | | quizli | 6 | | --- | --- | 7 | | Project Stats | [![GitHub Repo stars](https://img.shields.io/github/stars/pwenker/quizli?style=social)](https://github.com/pwenker/quizli) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/pwenker/quizli) ![Lines of code](https://img.shields.io/tokei/lines/github/pwenker/quizli) 8 | | Documentation | [![User Guide](https://img.shields.io/badge/docs-User%20Guide-brightgreen)](https://pwenker.github.io/quizli/user_guide) [![Learning Guide](https://img.shields.io/badge/docs-Learning%20Guide-brightgreen)](https://pwenker.github.io/quizli/learning_guide/) [![Demos](https://img.shields.io/badge/docs-Showcase-brightgreen)](https://pwenker.github.io/quizli/demos.html) | 9 | | Build status | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/pwenker/quizli/test_package) ![Codecov](https://img.shields.io/codecov/c/github/pwenker/quizli) | 10 | | Activity & Issue Tracking | ![GitHub last commit](https://img.shields.io/github/last-commit/pwenker/quizli) [![GitHub issues](https://img.shields.io/github/issues-raw/pwenker/quizli)](https://github.com/pwenker/quizli/issues?q=is%3Aopen+is%3Aissue) [![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/pwenker/quizli)](https://github.com/pwenker/quizli/issues?q=is%3Aissue+is%3Aclosed) | 11 | | PyPI | [![PyPI](https://img.shields.io/pypi/v/quizli)](https://pypi.org/project/quizli/) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/quizli) [![Downloads](https://pepy.tech/badge/quizli/month)](https://pepy.tech/project/quizli)| 12 | | News & Updates | [![Twitter Follow](https://img.shields.io/twitter/follow/PascalWenker?style=social)](https://twitter.com/PascalWenker) [![GitHub followers](https://img.shields.io/github/followers/pwenker?style=social)](https://github.com/pwenker)| 13 | 14 | ## Demo 15 | 16 | [![asciicast](https://asciinema.org/a/474148.svg)](https://asciinema.org/a/474148) 17 | 18 | ## :mortar_board: Learning Guide 19 | 20 | This guide teaches you how to effectively share a Python app with the open-source community. 21 | 22 | | | Learning Guide | 23 | | --- | --- | 24 | Interactive quiz app| [How to create an interactive Python quiz app?](https://pwenker.github.io/quizli/learning_guide/quiz.html) 25 | Command Line Interface | [How to add a CLI to your quiz app?](https://pwenker.github.io/quizli/learning_guide/cli.html) 26 | Documentation | [How to create a slick documentation for your app?](https://pwenker.github.io/quizli/learning_guide/documentation.html) 27 | Publishing | [How to build, manage and publish your Python package to PyPi?](https://pwenker.github.io/quizli/learning_guide/publishing.html) 28 | Testing | [How to test your app?](https://pwenker.github.io/quizli/learning_guide/testing.html) 29 | 30 | ## :rocket: User Guide 31 | 32 | This guide contains usage and reference material for the `quizli` app. 33 | 34 | | | User Guide | 35 | | --- | --- | 36 | CLI Reference | [Usage & reference for `quizli's` CLI](https://pwenker.github.io/quizli/user_guide/cli.html) 37 | Code Reference | [Usage & reference for `quizli's` source code](https://pwenker.github.io/quizli/code_reference/index.html) 38 | 39 | 40 | ## Quickstart 41 | 42 | ### :package: Installation 43 | 44 | Install quizli with [`pip`](https://pip.pypa.io/en/stable/getting-started/): 45 | 46 | ```console 47 | pip install quizli 48 | ``` 49 | 50 | ### :zap: Entrypoint 51 | To get help about `quizli's` commands open your console and type: 52 | 53 | ```console 54 | quizli --help 55 | ``` 56 | 57 | The same works for subcommands, e.g. : 58 | 59 | ```console 60 | quizli demo --help 61 | ``` 62 | -------------------------------------------------------------------------------- /quizli/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the entrypoint to quizli. 3 | 4 | We use the `typer` package to create a CLI. 5 | 6 | For more information see the [CLI reference](pwenker.github.io/quizli/user_guide/cli.html) 7 | """ 8 | 9 | 10 | from pathlib import Path 11 | from typing import Optional 12 | 13 | import typer 14 | from rich import print 15 | 16 | from quizli import Quiz, QuizConfig, QuizMode, examples 17 | from quizli.examples import QuizKind 18 | from quizli.session import QuizSession 19 | 20 | # {==(1)==} Here, we define our CLI app and give it a short but informative description 21 | # This will be shown on top of the help text when the app is launched with 22 | # `quizli --help`. 23 | 24 | app = typer.Typer( 25 | help="""Welcome to quizli 26 | 27 | An educational project teaching how to open-source an interactive Python quiz app 28 | 29 | Check out the project at: https://github.com/pwenker/quizli/ 30 | """ 31 | ) 32 | 33 | # {==(2)==} The `@app.command` decorator turns the function into a command for our CLI. 34 | # This way, the user can launch the `demo`-function with `quizli demo` 35 | # Here, as a result, the demo section of the docs will be opened in a new browser tab. 36 | @app.command() 37 | def demo(): 38 | """Open the documentation page in the browser""" 39 | import webbrowser 40 | 41 | webbrowser.open("https://pwenker.github.io/quizli/demos.html", new=2) 42 | 43 | 44 | # {==(3)==} Again, we define a command for our CLI: `quizli start`. 45 | # This time, we add a lot of parameters to the function, which will be translated 46 | # into CLI options. Here, Typer will make use of the type hints we provide. 47 | # For example, by supplying the `QuizKind` enumeration type hint to the 48 | # `quiz_name` parameter, Typer will automatically check that the user supplies a 49 | # valid variant when running the command: `quizli start --quiz-name ` 50 | # and it will show all existing variants in the help message `quizli start --help`. 51 | @app.command() 52 | def start( 53 | from_csv: Optional[Path] = typer.Option( 54 | None, exists=True, help="Read a quiz from a csv-file" 55 | ), 56 | mode: QuizMode = typer.Option( 57 | QuizMode.COMPLETE, 58 | help="Select the condition for the quiz to end", 59 | ), 60 | quiz_name: QuizKind = typer.Option( 61 | QuizKind.PYTHON_QUIZ, help="Select a built-in quiz" 62 | ), 63 | randomize: bool = typer.Option( 64 | True, 65 | "--randomize/--in-order", 66 | help="Shuffle the quiz before starting it", 67 | ), 68 | ): 69 | # {==(4)==} The docstring below will automatically be inserted into the help message 70 | # and can be shown with `quizli start --help`. The same is true for the help 71 | # strings we defined above for the parameters of the `start` function. 72 | """ 73 | Start the quiz with the configuration of your choice. 74 | 75 | ### Select Quiz Content 76 | 77 | There are 2 ways to create a quiz with `quizli`: 78 | 79 | 1. From a csv file containing question-answer pairs: e.g. `quizli start --from-csv examples/quiz.csv` 80 | 2. From a function in the `example` module: e.g. `quizli start --quiz-name python_quiz` 81 | 82 | ### Select Quiz Settings 83 | 84 | You can choose to 85 | 86 | - Shuffle the quiz with the `--randomize` flag (default), or keep it `--in-order`. 87 | 88 | - Terminate the quiz with the first wrong answer with `mode=sudden_death`, or continue on failure with `mode=complete` (default). 89 | """ 90 | 91 | config = QuizConfig(mode=mode, randomize=randomize) 92 | 93 | if from_csv is not None: 94 | quiz = Quiz.from_csv(Path(from_csv)) 95 | else: 96 | quiz = getattr(examples, quiz_name)() 97 | 98 | quiz_session = QuizSession(quiz, config) 99 | quiz_session.start() 100 | 101 | 102 | if __name__ == "__main__": 103 | app() 104 | -------------------------------------------------------------------------------- /quizli/examples.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains examples showing how to dynamically create a 'builtin'-quiz. 3 | 4 | - `python_quiz` 5 | - `binary_number_quiz` 6 | """ 7 | 8 | import importlib 9 | from pkgutil import iter_modules 10 | 11 | from rich import print 12 | from rich.prompt import IntPrompt, Prompt 13 | from rich.rule import Rule 14 | 15 | from quizli import Quiz, QuizItem, StrEnum 16 | from quizli.utils import em 17 | 18 | __all__ = [ 19 | "PyQuizCategory", 20 | "PythonModule", 21 | "QuizKind", 22 | "binary_number_quiz", 23 | "python_quiz", 24 | ] 25 | 26 | 27 | class QuizKind(StrEnum): 28 | """ 29 | This enumeration holds all quiz variants. 30 | 31 | By adding the name of your quiz function as a variant, 32 | it will automatically show up as an option in quizli's 33 | CLI. 34 | """ 35 | 36 | PYTHON_QUIZ = "python_quiz" 37 | BINARY_NUMBER_QUIZ = "binary_number_quiz" 38 | 39 | 40 | class PyQuizCategory(StrEnum): 41 | """ 42 | The category of the Python Quiz 43 | """ 44 | 45 | MODULES = "modules" 46 | OBJECTS = "objects" 47 | 48 | 49 | installed_modules = [mod.name for mod in iter_modules() if mod.ispkg] 50 | PythonModule = StrEnum("PythonModule", {mod.upper(): mod for mod in installed_modules}) 51 | 52 | 53 | def python_quiz() -> Quiz: 54 | 55 | """Make a quiz about Python 56 | 57 | This function let's the user interactively create one of the following 58 | two quiz types: 59 | 60 | 1. A quiz that shows the description of Python modules in the question field 61 | and asks the user for the name of the respective modules 62 | 2. A quiz that shows the description of Python objects of a chosen module 63 | in the question field and asks the user for the name of respective function/method/class. 64 | 65 | Returns: 66 | Quiz: The interactively created Python quiz 67 | 68 | """ 69 | print(Rule(f':snake: Welcome to the built-in {em("Python quiz")} :snake:')) 70 | 71 | category = Prompt.ask( 72 | f"Do you want to be quizzed on {em('modules')} or {em('objects')}?", 73 | choices=[c for c in PyQuizCategory], 74 | default=PyQuizCategory.OBJECTS, 75 | ) 76 | 77 | if category == PyQuizCategory.MODULES: 78 | quiz_items = [ 79 | QuizItem( 80 | question=importlib.import_module(module).__doc__, 81 | answer=importlib.import_module(module).__name__, 82 | ) 83 | for module in PythonModule 84 | ] 85 | return Quiz( 86 | name="What's the name of the module?", 87 | items=quiz_items, 88 | ) 89 | else: 90 | module_name = Prompt.ask( 91 | ":boom: Choose one of your installed modules :boom:", 92 | choices=[m for m in PythonModule], 93 | default="quizli", 94 | ) 95 | module = importlib.import_module(module_name) 96 | 97 | quiz_items = [ 98 | QuizItem( 99 | question=object.__doc__, 100 | answer=object.__name__, 101 | ) 102 | for _, object in module.__dict__.items() 103 | if hasattr(object, "__name__") 104 | ] 105 | return Quiz( 106 | name="What's the name of the object?", 107 | items=quiz_items, 108 | ) 109 | 110 | 111 | def binary_number_quiz() -> Quiz: 112 | """Make a quiz about binary numbers 113 | 114 | This function creates a quiz about conversion of 115 | binary numbers into their decimal representation. 116 | 117 | Returns: 118 | Quiz: The interactively created binary number quiz 119 | 120 | """ 121 | print( 122 | Rule( 123 | f':keycap_2: Welcome to the built-in {em("Binary number quiz")} :keycap_2:' 124 | ) 125 | ) 126 | 127 | largest_number = IntPrompt.ask( 128 | "What should be the largest number in the binary quiz?", 129 | default=1000, 130 | ) 131 | quiz_items = [ 132 | QuizItem(question=f"{x:b}", answer=str(x)) for x in range(largest_number) 133 | ] 134 | return Quiz( 135 | name="What's the decimal expression of this binary number?", 136 | items=quiz_items, 137 | ) 138 | -------------------------------------------------------------------------------- /docs/notebooks/quizli.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "5e45f10d", 6 | "metadata": {}, 7 | "source": [ 8 | "# Create a simple Quiz" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 1, 14 | "id": "e1889fc1", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import quizli" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "id": "2213d201", 25 | "metadata": {}, 26 | "outputs": [ 27 | { 28 | "name": "stdout", 29 | "output_type": "stream", 30 | "text": [ 31 | "\n", 32 | "This module contains the main building blocks of the quiz logic.\n", 33 | "\n", 34 | "* The `QuizItem` class represents a question-answer pair\n", 35 | "* The `Quiz` class contains a list of `QuizItem's` containing the quiz data\n", 36 | "* The `QuizConfig` class serves to configure quiz settings \n", 37 | "* The `QuizMode` enumeration serves to set the quiz mode\n", 38 | "\n", 39 | "\n" 40 | ] 41 | } 42 | ], 43 | "source": [ 44 | "print(quizli.quiz.__doc__)" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 3, 50 | "id": "0596f590", 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "from quizli import QuizItem, Quiz" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 4, 60 | "id": "03286c98", 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "questions_and_answers = {\n", 65 | " \"What is the meaning of life?\": 42,\n", 66 | " \"How to install quizli?\": \"pip install quizli\",\n", 67 | "}" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": 5, 73 | "id": "a28a1c2e", 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "example_quiz = Quiz.from_dict(questions_and_answers, name='Example Quiz')" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 6, 83 | "id": "fb3ff15f", 84 | "metadata": {}, 85 | "outputs": [ 86 | { 87 | "name": "stdout", 88 | "output_type": "stream", 89 | "text": [ 90 | "QuizItem(question='What is the meaning of life?', answer=42)\n", 91 | "QuizItem(question='How to install quizli?', answer='pip install quizli')\n" 92 | ] 93 | } 94 | ], 95 | "source": [ 96 | "example_quiz.show()" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 7, 102 | "id": "9796c14b", 103 | "metadata": {}, 104 | "outputs": [ 105 | { 106 | "name": "stdout", 107 | "output_type": "stream", 108 | "text": [ 109 | "Question: What is the meaning of life?\n" 110 | ] 111 | }, 112 | { 113 | "data": { 114 | "text/html": [ 115 | "
\n"
116 |       ],
117 |       "text/plain": []
118 |      },
119 |      "metadata": {},
120 |      "output_type": "display_data"
121 |     },
122 |     {
123 |      "name": "stdout",
124 |      "output_type": "stream",
125 |      "text": [
126 |       "Answer: 42\n",
127 |       "Question: How to install quizli?\n"
128 |      ]
129 |     },
130 |     {
131 |      "data": {
132 |       "text/html": [
133 |        "
\n"
134 |       ],
135 |       "text/plain": []
136 |      },
137 |      "metadata": {},
138 |      "output_type": "display_data"
139 |     },
140 |     {
141 |      "name": "stdout",
142 |      "output_type": "stream",
143 |      "text": [
144 |       "Answer: pip install quizli\n"
145 |      ]
146 |     }
147 |    ],
148 |    "source": [
149 |     "for quiz_item in example_quiz.items:\n",
150 |     "    quiz_item.ask()"
151 |    ]
152 |   }
153 |  ],
154 |  "metadata": {
155 |   "kernelspec": {
156 |    "display_name": "Python 3 (ipykernel)",
157 |    "language": "python",
158 |    "name": "python3"
159 |   },
160 |   "language_info": {
161 |    "codemirror_mode": {
162 |     "name": "ipython",
163 |     "version": 3
164 |    },
165 |    "file_extension": ".py",
166 |    "mimetype": "text/x-python",
167 |    "name": "python",
168 |    "nbconvert_exporter": "python",
169 |    "pygments_lexer": "ipython3",
170 |    "version": "3.8.0"
171 |   }
172 |  },
173 |  "nbformat": 4,
174 |  "nbformat_minor": 5
175 | }
176 | 


--------------------------------------------------------------------------------
/docs/learning_guide/testing.md:
--------------------------------------------------------------------------------
  1 | # How to test your app?
  2 | 
  3 | !!! abstract "Learning Objectives"
  4 |     By the end of this section (if you work on the assignments), you should know how to:
  5 | 
  6 |     * Create basic tests for your CLI with `pytest` and `typer`
  7 |     * Test your APIs with `doctest` (via `pytest`)
  8 |     * Add a CI/CD pipeline for automatic testing
  9 |     
 10 | You made it to the last section of the learning guide! Kudos! :partying_face:
 11 | 
 12 | Testing software is a wonderful but complicated endeavor. A thorough discussion of all the facets of testing is beyond the scope of this section.
 13 | 
 14 | But we do want to emphasize the crucial role that testing plays in software development. 
 15 | So we at least provide you with some basic examples and then point to resources for further learning.
 16 | 
 17 | Since this is the last chapter of the learning guide, we are going to challenge you a bit more:
 18 | we leave out extensive explanations and instead let you work through assignments. We hope this will help you deepening your understanding and prepare you for your next
 19 | steps on your journey to become a great Pythonista and open-source contributor! :material-language-python:
 20 | 
 21 | ## Testing your CLI with `pytest` and `Typer`
 22 | 
 23 | !!! assignment "Assignment 1"
 24 |     - a) Read through: [https://typer.tiangolo.com/tutorial/testing/](https://typer.tiangolo.com/tutorial/testing/)
 25 |     - b) Read through: [https://docs.pytest.org/en/7.0.x/getting-started.html](https://docs.pytest.org/en/7.0.x/getting-started.html)
 26 |     - c) Add useful tests to `tests/test_quizli.py` (shown below) to improve the code coverage.
 27 |     
 28 | ``` py title="test_quizli.py"
 29 | 
 30 | --8<-- "tests/test_quizli.py"
 31 | ```
 32 | 
 33 | ## Testing your docstring examples with `doctest`
 34 | 
 35 | !!! assignment "Assignment 2"
 36 |     - Read through: [`doctests` - Test interactive Python examples](https://docs.python.org/3/library/doctest.html)
 37 |     - Read through: [`pytest` - Doctest integration for modules and test files](https://docs.pytest.org/en/6.2.x/doctest.html)
 38 | 
 39 | ???+ question "What is `doctest` good for?"
 40 |     !!! quote
 41 |         The doctest module searches for pieces of text that look like interactive Python sessions, and then executes those sessions to verify that they work exactly as shown. 
 42 |         There are several common ways to use doctest:
 43 | 
 44 |         - To check that a module’s docstrings are up-to-date by verifying that all interactive examples still work as documented.
 45 |         - To perform regression testing by verifying that interactive examples from a test file or a test object work as expected.
 46 |         - To write tutorial documentation for a package, liberally illustrated with input-output examples. Depending on whether the examples or the expository text are emphasized, this has the flavor of “literate testing” or “executable documentation”.
 47 | 
 48 | !!! example 
 49 |     ```
 50 |     @dataclass
 51 |     class QuizItem:
 52 |         """Represents a quiz item - a question-answer pair
 53 | 
 54 |         Attributes:
 55 |             question: The question of the quiz item
 56 |             answer: The answer of the quiz item
 57 | 
 58 |     {==
 59 |         Examples:
 60 |             >>> quiz_item = QuizItem(question='What is the meaning of life?', answer=42)
 61 | 
 62 |             >>> quiz_item
 63 |             QuizItem(question='What is the meaning of life?', answer=42)
 64 | 
 65 |             >>> quiz_item.question
 66 |             'What is the meaning of life?'
 67 | 
 68 |             >>> quiz_item.answer
 69 |             42
 70 |         """
 71 |     ==}
 72 | 
 73 |     (...)
 74 |     ```
 75 |     
 76 | !!! hint
 77 |     We can run`pytest` with the `--doctest-modules` flag to test whether all docstring examples are correct and up-to-date.
 78 | 
 79 | ## Automate testing with a CI/CD pipeline
 80 | 
 81 | !!! assignment "Assignment 3"
 82 |     - a) Read through Github's [CI/CD: The what, why, and how](https://resources.github.com/ci-cd/)
 83 |     - b) Read through: [https://github.com/snok/install-poetry](https://github.com/snok/install-poetry)
 84 |     - c) Read through `.github/workflows/test_package.yml` (shown below) and then explain it to someone.
 85 |     
 86 | ``` yaml title="CI/CD workflow"
 87 | 
 88 | --8<-- ".github/workflows/test_package.yml"
 89 | ```
 90 | 
 91 | 
 92 | ## Real-world testing
 93 | 
 94 | We already told you that testing is hard. And there is a lot to learn to do it right. 
 95 | Before we part, here is a list with key concepts you can explore to ease your way into the world of testing.
 96 | 
 97 | !!! assignment "Assignment 4: Key Concepts in the testing world"
 98 |     - a) What is **test-driven development**?
 99 |     - b) What is **property-based testing**?
100 |     - c) How can you utilize **tests as documentation**?
101 |     - d) What is **unit testing**? And what is **integration testing**?
102 |     - e) What is **code coverage**?
103 | 
104 | 


--------------------------------------------------------------------------------
/quizli/quiz.py:
--------------------------------------------------------------------------------
  1 | """
  2 | This module contains the main building blocks of the quiz logic.
  3 | 
  4 | * The `QuizItem` class represents a question-answer pair
  5 | * The `Quiz` class contains a list of `QuizItem's` containing the quiz data
  6 | * The `QuizConfig` class serves to configure quiz settings 
  7 | * The `QuizMode` enumeration serves to set the quiz mode
  8 | 
  9 | """
 10 | 
 11 | import csv
 12 | import random
 13 | from dataclasses import dataclass
 14 | from enum import Enum
 15 | from pathlib import Path
 16 | from typing import Dict, List
 17 | 
 18 | from rich.prompt import Prompt
 19 | 
 20 | __all__ = ["StrEnum", "QuizMode", "QuizConfig", "QuizItem", "Quiz"]
 21 | 
 22 | 
 23 | class StrEnum(str, Enum):
 24 |     """
 25 |     We define this `StrEnum` class for convenience.
 26 |     Later on we will define a few Enums that will inherit from it,
 27 |     and we will then use them in our CLI as parameter choices:
 28 | 
 29 |     Resources:
 30 |         * https://docs.python.org/3/library/enum.html
 31 |         * https://typer.tiangolo.com/tutorial/parameter-types/enum/
 32 |     """
 33 | 
 34 | 
 35 | class QuizMode(StrEnum):
 36 |     """Select the mode of the quiz
 37 | 
 38 |     Attributes:
 39 |         SUDDEN_DEATH: Finish the quiz if user answers incorrectly
 40 |         COMPLETE: Go through the whole quiz in any case
 41 |     """
 42 | 
 43 |     SUDDEN_DEATH = "sudden_death"
 44 |     COMPLETE = "complete"
 45 | 
 46 | 
 47 | @dataclass
 48 | class QuizConfig:
 49 |     """Allows configuring the quiz settings
 50 | 
 51 |     Attributes:
 52 |         mode: Sets the mode of the quiz (See `QuizMode`)
 53 |         randomize: Whether the quiz should be shuffled
 54 | 
 55 |     """
 56 | 
 57 |     mode: QuizMode = QuizMode.SUDDEN_DEATH
 58 |     randomize: bool = True
 59 | 
 60 | 
 61 | @dataclass
 62 | class QuizItem:
 63 |     """Represents a quiz item - a question-answer pair
 64 | 
 65 |     Attributes:
 66 |         question: The question of the quiz item
 67 |         answer: The answer of the quiz item
 68 | 
 69 |     Examples:
 70 |         >>> quiz_item = QuizItem(question='What is the meaning of life?', answer=42)
 71 | 
 72 |         >>> quiz_item
 73 |         QuizItem(question='What is the meaning of life?', answer=42)
 74 | 
 75 |         >>> quiz_item.question
 76 |         'What is the meaning of life?'
 77 | 
 78 |         >>> quiz_item.answer
 79 |         42
 80 |     """
 81 | 
 82 |     question: str
 83 |     answer: str
 84 | 
 85 |     def ask(self, show_question: bool = True) -> bool:
 86 |         """Show a question and prompt for an answer
 87 | 
 88 |         This method shows the user a question and
 89 |         returns whether the user typed the correct.
 90 |         answer.
 91 | 
 92 |         Args:
 93 |             show_question: Whether to show the question
 94 |         Returns:
 95 |             bool: Whether the question as answered correctly
 96 |         """
 97 |         if show_question:
 98 |             print(f"Question: {self.question}")
 99 |         guess = Prompt.ask("Answer")
100 |         return guess == self.answer
101 | 
102 | 
103 | @dataclass
104 | class Quiz:
105 |     """
106 |     The `Quiz` dataclass contains the quiz data along with some functionality.
107 | 
108 |     Attributes:
109 |         items: The items of the quiz (question-answer pairs)
110 |         name: The name of the quiz
111 | 
112 |     Examples:
113 |         Create a quiz from a csv file
114 |         >>> quiz = Quiz.from_csv(file_name='examples/quiz.csv', name='Example Quiz')
115 | 
116 | 
117 |     """
118 | 
119 |     items: List[QuizItem]
120 |     name: str = "Quiz"
121 | 
122 |     def __len__(self):
123 |         return len(self.items)
124 | 
125 |     @classmethod
126 |     def from_dict(cls, questions_and_answers: Dict, name: str) -> "Quiz":
127 |         """Create a Quiz instance from a dictionary
128 | 
129 |         This classmethod accepts a dictionary and a name
130 |         and then instantiates and returns a quiz from it.
131 | 
132 |         Args:
133 |             questions_and_answers: The dictionary containing questions and answers
134 |             name: The name of the quiz
135 | 
136 |         Returns:
137 |             Quiz: The quiz instance generated from the dictionary
138 |         """
139 |         return cls(
140 |             items=[QuizItem(q, a) for q, a in questions_and_answers.items()], name=name
141 |         )
142 | 
143 |     @classmethod
144 |     def from_csv(cls, file_name: Path, name: str = "") -> "Quiz":
145 |         """Create a Quiz instance from a csv file
146 | 
147 |         This classmethod accepts a file path and an optional
148 |         name and then instantiates and returns a quiz from it.
149 | 
150 |         Args:
151 |             file_name: The path of the csv file
152 |             name: An optional name for the quiz. If not given,
153 |                 it will fallback to the file name.
154 | 
155 |         Returns:
156 |             "Quiz": The quiz instance generated from the csv file
157 |         """
158 |         quiz_name = name if name else file_name.stem
159 |         with open(file_name, newline="") as f:
160 |             quiz = cls(items=[QuizItem(q, a) for q, a in csv.reader(f)], name=quiz_name)
161 |         return quiz
162 | 
163 |     def show(self) -> None:
164 |         """Show the items of the quiz"""
165 |         for item in self.items:
166 |             print(item)
167 | 
168 |     def shuffle(self) -> None:
169 |         """Shuffle the items of the quiz"""
170 |         random.shuffle(self.items)
171 | 


--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
  1 | ---
  2 | #  ╭──────────────────────────────────────────────────────────╮
  3 | #  │                  Adding site information                 │
  4 | #  ╰──────────────────────────────────────────────────────────╯
  5 |   
  6 | site_name: quizli
  7 | site_url: https://pwenker.github.io/quizli
  8 | site_description: An educational project teaching how to create an open-source Python package with the example of an interactive quiz app
  9 |   
 10 | #  ╭──────────────────────────────────────────────────────────╮
 11 | #  │                  Adding a git repository                 │
 12 | #  ╰──────────────────────────────────────────────────────────╯
 13 | # See: https://squidfunk.github.io/mkdocs-material/setup/adding-a-git-repository/
 14 |  
 15 | repo_url: https://github.com/pwenker/quizli
 16 | repo_name: pwenker/quizli
 17 | edit_uri: edit/master/docs/
 18 |   
 19 | docs_dir: docs
 20 | use_directory_urls: false
 21 | 
 22 | #  ╭──────────────────────────────────────────────────────────╮
 23 | #  │                   Setting up the footer                  │
 24 | #  ╰──────────────────────────────────────────────────────────╯
 25 | # See: https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-footer/
 26 |  
 27 | extra:
 28 |   social:
 29 |   - icon: fontawesome/brands/github
 30 |     link: "https://www.github.com/pwenker"
 31 |   - icon: fontawesome/brands/twitter
 32 |     link: "https://www.twitter.com/PascalWenker"
 33 |   - icon: fontawesome/brands/linkedin
 34 |     link: "https://www.linkedin.com/in/pascal-wenker-25a568125/"
 35 |   - icon: fontawesome/brands/youtube
 36 |     link: "https://www.youtube.com/channel/UCYxDcdFLW-AMoHEfh7pG-oA"
 37 |   disqus: "www-pwenker-com" 
 38 |     
 39 | copyright: Copyright © 2021 - 2022 Pascal Wenker
 40 | 
 41 | #  ╭──────────────────────────────────────────────────────────╮
 42 | #  │                Setting up the colorscheme                │
 43 | #  ╰──────────────────────────────────────────────────────────╯
 44 | # Here we set the color scheme and also enable toggling between a light and a dark mode.
 45 | # See: https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/
 46 |  
 47 | theme:
 48 |   name: material
 49 |   palette:
 50 |     - scheme: default
 51 |       toggle:
 52 |         icon: material/weather-night
 53 |         name: Switch to dark mode
 54 |     - scheme: slate
 55 |       toggle:
 56 |         icon: material/weather-sunny
 57 |         name: Switch to light mode
 58 |           
 59 |   icon:
 60 |     #  ╭──────────────────────────────────────────────────────────╮
 61 |     #  │                Changing the logo and icons               │
 62 |     #  ╰──────────────────────────────────────────────────────────╯
 63 |     # See : https://squidfunk.github.io/mkdocs-material/setup/changing-the-logo-and-icons/
 64 |     logo: material/head-question-outline
 65 |     # See: https://squidfunk.github.io/mkdocs-material/setup/adding-a-git-repository/
 66 |     repo: fontawesome/brands/github
 67 |   # See: https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/
 68 |   features:
 69 |     - navigation.tabs
 70 |     - navigation.indexes
 71 |       
 72 |   # Uncomment the line below to prevent typefaces from being loaded from Google Fonts
 73 |   # See: https://squidfunk.github.io/mkdocs-material/setup/changing-the-fonts/#autoloading
 74 |   # font: false
 75 |     
 76 | 
 77 | #  ╭──────────────────────────────────────────────────────────╮
 78 | #  │                   Setup MkDocs Plugins                   │
 79 | #  ╰──────────────────────────────────────────────────────────╯
 80 | # See: https://www.mkdocs.org/dev-guide/plugins/
 81 | plugins:
 82 |     # The built-in search plugin (https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/) 
 83 |     - search 
 84 |     - mkdocs-jupyter:
 85 |         include_source: false
 86 |         execute: false
 87 |     - mkdocstrings: # This is used to generate documentation of source code
 88 |            handlers:
 89 |                python:
 90 |                    setup_commands:
 91 |                        - import sys
 92 |                        - sys.path.append('../')
 93 |                    selection:
 94 |                        new_path_syntax: true
 95 |     # See: https://squidfunk.github.io/mkdocs-material/setup/adding-a-git-repository/#revision-date
 96 |     - git-revision-date-localized:
 97 |         fallback_to_build_date: true
 98 |         enable_creation_date: false
 99 |           
100 | 
101 | #  ╭──────────────────────────────────────────────────────────╮
102 | #  │               Enabling Markdown extensions               │
103 | #  ╰──────────────────────────────────────────────────────────╯
104 | #   https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown/
105 | markdown_extensions:
106 |   # The Table of Contents extension automatically generates a table of contents from a document, 
107 |   # which Material for MkDocs will render as part of the resulting page.
108 |   - toc:
109 |       permalink: true
110 |   # - codehilite:
111 |   #     guess_lang: false
112 |   - admonition
113 |   - pymdownx.details
114 |   - extra
115 |   - pymdownx.superfences
116 |   - pymdownx.tabbed
117 |   - def_list
118 |   - pymdownx.tasklist:
119 |       custom_checkbox: true
120 |   - pymdownx.highlight:
121 |       anchor_linenums: true
122 |   - pymdownx.inlinehilite
123 |   - pymdownx.snippets
124 |   - pymdownx.critic
125 |     
126 |   # Adding emoji support
127 |   # See: https://squidfunk.github.io/mkdocs-material/reference/icons-emojis/#configuration
128 |   - pymdownx.emoji:
129 |       emoji_index: !!python/name:materialx.emoji.twemoji # (1)!
130 |       emoji_generator: !!python/name:materialx.emoji.to_svg
131 | 
132 |     
133 | #  ╭──────────────────────────────────────────────────────────╮
134 | #  │                     Set up navigation                    │
135 | #  ╰──────────────────────────────────────────────────────────╯
136 | # See: https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/
137 |  
138 | nav:
139 |    - User Guide:
140 |      - user_guide/index.md
141 |      - Command Line Interface: user_guide/cli.md
142 |      - Code Reference:
143 |        - code_reference/index.md
144 |        - Quiz: code_reference/quiz.md
145 |        - Layout: code_reference/layout.md
146 |        - Session: code_reference/session.md
147 |        - Examples: code_reference/examples.md
148 |        - CLI: code_reference/cli.md
149 |    - Learning Guide:
150 |      - learning_guide/index.md
151 |      - Interactive Quiz App: learning_guide/quiz.md
152 |      - Command Line Interface: learning_guide/cli.md
153 |      - Documentation: learning_guide/documentation.md
154 |      - Publishing: learning_guide/publishing.md
155 |      - Testing: learning_guide/testing.md
156 |    - Notebooks:
157 |      - Quizli: notebooks/quizli.ipynb
158 |    - Demos:
159 |      - demos.md
160 | 


--------------------------------------------------------------------------------
/quizli/layout.py:
--------------------------------------------------------------------------------
  1 | """
  2 | This module contains all the layout logic.
  3 | 
  4 | - We define a `QuizLayoutBase` abstract base class.
  5 | - We define a `QuizLayout` base class.
  6 | 
  7 | """
  8 | 
  9 | from abc import ABC, abstractmethod
 10 | from dataclasses import dataclass
 11 | from typing import Optional, Union
 12 | 
 13 | from rich import print
 14 | from rich.align import Align
 15 | from rich.layout import Layout
 16 | from rich.panel import Panel
 17 | from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
 18 | 
 19 | from quizli import QuizItem
 20 | from quizli.utils import em, motivate
 21 | 
 22 | __all__ = ["QuizLayoutBase", "FancyQuizLayout"]
 23 | 
 24 | 
 25 | class QuizLayoutBase(ABC):
 26 |     """This abstract base class serves as blueprint for `QuizLayout` classes"""
 27 | 
 28 |     @abstractmethod
 29 |     def initialize(self, quiz_name: str, n_questions: int) -> None:
 30 |         """Initializes the quiz layout
 31 | 
 32 |         Args:
 33 |             quiz_name (str): Name of the quiz
 34 |             n_questions (int): Number of questions in the quiz
 35 |         """
 36 |         pass
 37 | 
 38 |     @abstractmethod
 39 |     def show_question(self, item: QuizItem, idx: int) -> None:
 40 |         """Update layout to show the question
 41 | 
 42 |         Args:
 43 |             item (QuizItem): The quiz item to be asked
 44 |             idx (int): The index of the quiz item in the quiz
 45 |         """
 46 |         pass
 47 | 
 48 |     @abstractmethod
 49 |     def reveal_answer(self, item: QuizItem, answer_correct: bool) -> None:
 50 |         """Update layout to show answer
 51 | 
 52 |         Args:
 53 |             item (QuizItem): The quiz item that was asked
 54 |             answer_correct (bool): Whether the question was answered correctly
 55 |         """
 56 |         pass
 57 | 
 58 |     def show_results(self, has_won: bool) -> None:
 59 |         """Update the layout to show the quiz result
 60 | 
 61 |         Args:
 62 |             has_won (bool): Whether the user has won
 63 |         """
 64 |         pass
 65 | 
 66 | 
 67 | @dataclass
 68 | class FancyQuizLayout(QuizLayoutBase):
 69 |     layout: Optional[Layout] = None
 70 | 
 71 |     def print(self):
 72 |         print(self.layout)
 73 | 
 74 |     def initialize(self, quiz_name: str, n_questions: int) -> None:
 75 |         self.quiz_name = quiz_name
 76 |         self.n_questions = n_questions
 77 | 
 78 |         if self.layout is None:
 79 |             self.layout = self._make_quiz_layout()
 80 |         self.statsbar = self._make_statsbar(n_questions)
 81 |         self.progressbar = self._make_progressbar(n_questions)
 82 | 
 83 |         self.layout["stats"].update(Panel(self.statsbar, title="Stats"))
 84 |         self.layout["progress"].update(Panel(self.progressbar))
 85 |         self._reset_dialog()
 86 | 
 87 |         print(self.layout)
 88 | 
 89 |     def show_question(self, item: QuizItem, idx: int) -> None:
 90 |         self._reset_answer()
 91 |         self._reset_dialog()
 92 |         self.layout["question"].update(
 93 |             Panel(
 94 |                 Align.center(f"[bold]{item.question}", vertical="middle"),
 95 |                 title=f"[blue]Question {idx}/{self.n_questions}[/blue] | {self.quiz_name}",
 96 |                 border_style="green",
 97 |                 highlight=True,
 98 |             )
 99 |         )
100 |         print(self.layout)
101 | 
102 |     def reveal_answer(self, item: QuizItem, answer_correct: bool) -> None:
103 |         color = "bold green" if answer_correct else "bold red"
104 |         self.layout["answer"].update(
105 |             Panel(
106 |                 Align.center(f"[{color}]{item.answer}", vertical="middle"),
107 |                 title=f"[blue]Answer",
108 |                 border_style="green",
109 |                 highlight=True,
110 |             )
111 |         )
112 |         print(self.layout)
113 | 
114 |         if answer_correct:
115 |             self.layout["dialog"].update(
116 |                 Panel(
117 |                     Align.center(
118 |                         f":fire: [{color}]That's correct! :fire: [/{color}] {motivate()}",
119 |                         vertical="middle",
120 |                     ),
121 |                 )
122 |             )
123 |             # Advance 'correct' statsbar
124 |             self.statsbar.advance(0)
125 |         else:
126 |             self.layout["dialog"].update(
127 |                 Panel(
128 |                     Align.center(
129 |                         f"[red bold]:cross_mark: That's wrong :cross_mark: [/red bold]",
130 |                         vertical="middle",
131 |                     ),
132 |                 )
133 |             )
134 |             # Advance 'wrong' statsbar
135 |             self.statsbar.advance(1)
136 | 
137 |         self.progressbar.advance(0)
138 |         self.print()
139 | 
140 |     def show_results(self, has_won: Optional[bool] = None) -> None:
141 |         if has_won is None:
142 |             result_message = f"[blue bold]See you soon ![/blue bold]"
143 |         elif has_won:
144 |             result_message = f":trophy: [green bold]You won![/green bold] :trophy: See you soon, champion!"
145 |         else:
146 |             result_message = (
147 |                 f"[red bold]Game Over![/red bold] :disappointed_face: See you soon !"
148 |             )
149 | 
150 |         n_correct = self.statsbar.tasks[0].completed
151 |         n_false = self.statsbar.tasks[1].completed
152 |         n_total = self.progressbar.tasks[0].total
153 | 
154 |         result_message += f"\nYou have answered {em(n_correct, 'green')} question(s) correctly and {em(n_false, 'red')} incorrectly"
155 |         l = Layout(
156 |             Panel(
157 |                 Align.center(result_message, vertical="middle"),
158 |                 title=f"[blue]Results",
159 |             ),
160 |         )
161 |         print(l)
162 | 
163 |     def _reset_answer(self) -> None:
164 |         self.layout["answer"].update(
165 |             Panel(
166 |                 Align.center(
167 |                     f":question-emoji::question-emoji::question-emoji:",
168 |                     vertical="middle",
169 |                 ),
170 |                 title=f"[blue]Answer",
171 |             )
172 |         )
173 | 
174 |     def _reset_dialog(self) -> None:
175 |         self.layout["dialog"].update(
176 |             Panel(
177 |                 Align.center(
178 |                     f"[bold blue]Type in your answer!",
179 |                     vertical="middle",
180 |                 ),
181 |             )
182 |         )
183 | 
184 |     @staticmethod
185 |     def _make_quiz_layout() -> Layout:
186 |         layout = Layout(name="root")
187 |         layout.split_column(
188 |             Layout(name="progress"),
189 |             Layout(name="question"),
190 |             Layout(name="answer"),
191 |             Layout(name="footer"),
192 |         )
193 |         layout["progress"].ratio = 2
194 |         layout["question"].ratio = 8
195 |         layout["answer"].ratio = 8
196 |         layout["footer"].ratio = 3
197 | 
198 |         layout["footer"].split_row(Layout(name="dialog"), Layout(name="stats"))
199 |         layout["dialog"].ratio = 2
200 |         layout["stats"].ratio = 1
201 |         return layout
202 | 
203 |     @staticmethod
204 |     def _make_progressbar(total: int) -> Progress:
205 |         progressbar = Progress(
206 |             "{task.description}",
207 |             BarColumn(),
208 |             TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
209 |             expand=True,
210 |         )
211 |         progressbar.add_task("[cyan]Quiz Progress", total=total)
212 |         return progressbar
213 | 
214 |     @staticmethod
215 |     def _make_statsbar(total: int) -> Progress:
216 |         statsbar = Progress(
217 |             "{task.description}",
218 |             SpinnerColumn(),
219 |             BarColumn(),
220 |             TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
221 |         )
222 |         statsbar.add_task("[green]Correct", total=total)
223 |         statsbar.add_task("[red]Wrong", total=total)
224 |         return statsbar
225 | 


--------------------------------------------------------------------------------
/docs/learning_guide/documentation.md:
--------------------------------------------------------------------------------
  1 | # Documentation with [**Material for MkDocs**](https://squidfunk.github.io/mkdocs-material/)
  2 | 
  3 | !!! abstract "Learning Objectives"
  4 |     By the end of this section, you should be able to:
  5 | 
  6 |     * Explain why **Material for MkDocs** is an optimal choice to build your documentation
  7 |     * Know how to navigate the official documentation of **Material for MkDocs** 
  8 |     * Build your own documentation from scratch 
  9 |     * Explain how the `quizli` documentation was built
 10 | 
 11 | ## What is **Material for MkDocs**?
 12 | 
 13 | 
 14 | **Material for MkDocs** is a [**Material Design**](https://material.io/) theme for [**MkDocs**](https://www.mkdocs.org/).
 15 | 
 16 | !!! info
 17 |     !!! quote "Material"
 18 |         A design system created by Google to help teams build high-quality digital experiences for Android, iOS, Flutter, and the web.
 19 |     !!! quote "MkDocs"
 20 |         A fast, simple and downright gorgeous static site generator that's geared towards building project documentation. Documentation source files are written in Markdown, and configured with a single YAML configuration file.
 21 | 
 22 | With **Material for MkDocs** you can, quoting it's Github Readme, create
 23 | > a branded static site from a set of Markdown files to host the documentation of your Open Source or commercial project - customizable, searchable, mobile-friendly, 50+ languages. Set up in 5 minutes.
 24 | 
 25 | !!! tip "Alternative themes"
 26 |     If you don't like the look of the Material theme, there is a list of
 27 |     [alternative MkDocs themes](https://www.mkdocs.org/user-guide/choosing-your-theme/)
 28 |     
 29 | ## Why **Material for MkDocs**?
 30 | 
 31 | Here is a list of features that make **Material for MkDocs** a great choice: 
 32 | 
 33 | 1. Geared towards (technical) project documentation
 34 | 2. Future-proof & simple to use thanks to using Markdown source
 35 | 3. Generates a SEO-friendly site
 36 | 4. It's Open-Source & has a permissive license (MIT)
 37 | 5. It's customizable (via `JavaScript` and `CSS`) and extendable (via plugins)
 38 | 6. Easy to publish via Gitlab/Github Pages
 39 | 
 40 | !!! info "Alternative static site generators"
 41 | 
 42 |     Read the following section in **Material for MkDocs'** documentation to see how it compares to alternative
 43 |     static site generators: [Alternatives](https://squidfunk.github.io/mkdocs-material/alternatives/)
 44 | 
 45 | To prove to you how easy it is to generate a basic documentation for your existing project, we point to the following example:
 46 | 
 47 | ### Example
 48 | !!! example "Minimal Example"
 49 | 
 50 |     1. Install **Material for MkDocs** with:
 51 |     ```
 52 |     pip install mkdocs-material
 53 |     ```
 54 |     2. Move your existing Markdown files into a `docs/` folder
 55 |     3. Add a `mkdocs.yml` file in your project root with the minimal content:
 56 | 
 57 |     ``` yaml
 58 |     theme:
 59 |       name: material
 60 |     ```
 61 | 
 62 |     !!! hint
 63 |         For new projects steps 2. and 3. can be even abbreviated further with the `mkdocs new .` command
 64 | 
 65 |     :rocket: That's all you need. 
 66 |     Just serve your docs with `mkdocs serve` and enjoy them in your browser.
 67 | 
 68 | Amazingly, for many projects this simple setup already leads to a great documentation since **Material for MkDocs** uses reasonable defaults.
 69 | 
 70 | Still, there is plenty room for improvement. So in the following subsections we walk you through some customizations to build a more sophisticated documentation.
 71 | 
 72 | ## How to Create your own Documentation from Scratch?
 73 | 
 74 | Instead of (doing a bad job in) rewriting the existing documentation of **Material for MkDocs**, we will instead point out and guide you through the relevant sections.
 75 | 
 76 | ### 1. The Getting Started Section
 77 | 
 78 | If you are new to **Material for MkDocs**, first check out its [Getting Started section](https://squidfunk.github.io/mkdocs-material/getting-started/).
 79 | It contains installation instructions, an example of how to create a minimal documentation, and a few brief discussions of more advanced topics like publishing your documentation.
 80 | At the end of it, you should already be equipped with a bare-bones documentation.
 81 | 
 82 | ### 2. The Setup Section
 83 | 
 84 | Next, I suggest you read through the [setup section](https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/) in order to add more features to
 85 | your documentation and to customize it.
 86 | 
 87 | 
 88 | !!! info
 89 |     Below you can see all customization options that currently exist. 
 90 |     
 91 |     I added a checkmark for those options that are used by `quizli`.
 92 |     
 93 | * [x] Changing the colors
 94 | * [ ] Changing the fonts
 95 | * [ ] Changing the language
 96 | * [x] Changing the logo and icons
 97 | * [x] Setting up navigation
 98 | * [x] Setting up site search
 99 | * [ ] Setting up site analytics
100 | * [ ] Setting up social cards
101 | * [ ] Setting up tags
102 | * [ ] Setting up versioning
103 | * [ ] Setting up the header
104 | * [x] Setting up the footer
105 | * [x] Adding a git repository
106 | * [x] Adding a comment system
107 | 
108 | ### 3. The Reference Section
109 | 
110 | I bet at this point your documentation looks quite amazing already, but we can improve it even more by leveraging the [reference section](https://squidfunk.github.io/mkdocs-material/reference/).
111 | 
112 | !!! info 
113 |     As of time of writing, the reference section comprises the following parts.
114 |     
115 |     Again we add checkmarks for those features that we used for `quizli's` docs.
116 | 
117 | * [ ] Abbreviations
118 | * [x] Admonitions
119 | * [ ] Annotations
120 | * [ ] Buttons
121 | * [x] Code blocks
122 | * [x] Content tabs
123 | * [x] Data tables
124 | * [ ] Diagrams
125 | * [x] Footnotes
126 | * [x] Formatting
127 | * [x] Icons + Emojis
128 | * [ ] Images
129 | * [x] Lists
130 | * [ ] MathJax
131 | 
132 | 
133 | ## Learning by Example: `quizli`
134 | 
135 | I encourage you to browse through `quizli's` documentation and whenever you see something you like,
136 | or you are curious of how things are done, take a look at the corresponding sections in the
137 | [`docs`](https://github.com/pwenker/quizli/tree/main/docs) folder.
138 | 
139 | !!! tip "Tip: Learning by Example"
140 | 
141 |     Many popular projects use **Material for MkDocs**. 
142 |     
143 |     Their creators are often experienced and creative developers who
144 |     put a lot of effort into their documentation. 
145 |     
146 |     Hence a great way to learn is to read through their documentation
147 |     and see what you can adapt.
148 | 
149 |     Some examples:
150 |     
151 |     - [FastAPI](https://fastapi.tiangolo.com/)
152 |     - [AWS Copilot CLI](https://aws.github.io/copilot-cli/)
153 |     - [Pydantic](https://pydantic-docs.helpmanual.io/)
154 |     - [Traefik](https://doc.traefik.io/traefik/)
155 | 
156 |     And [plenty more](https://github.com/squidfunk/mkdocs-material#trusted-by-)!
157 | 
158 | 
159 | ### Configuring the documentation
160 | 
161 | Below you find the configuration settings of `quizli's` documentation. 
162 | 
163 | For each block of settings, I added a link to the corresponding 
164 | section in the **Material for MkDocs** documentation for further reference. 
165 | 
166 | 
167 | ``` yaml title="The configuration file: mkdocs.yml "
168 | --8<-- "mkdocs.yml"
169 | ```
170 | 
171 | ### Publish your documentation
172 | 
173 | !!! info "Info: Gitlab pages support"
174 | 
175 |     While our example below shows a Github Workflow, you could publish
176 |     your documentation equally well with Gitlab pages.
177 | 
178 |     This might come especially handy if you want to host your own private
179 |     documentation.
180 | 
181 |     Instructions can be found again in the
182 |     [Publishing your site](https://squidfunk.github.io/mkdocs-material/publishing-your-site/) 
183 |     section
184 | 
185 | There is a dedicated section in the **Material for MkDocs** documentation on how to [Publish your documentation](https://squidfunk.github.io/mkdocs-material/publishing-your-site/).
186 | 
187 | For our purposes, we automate the deployment of the project documentation with a GitHub Actions workflow.
188 | 
189 | ``` yaml title="Publishing your docs: .github/workflows/deploy_docs.yml "
190 | --8<-- ".github/workflows/deploy_docs.yml"
191 | ```
192 | 
193 | This way, our documentation is conveniently updated every time we push changes to our git repository.
194 | 
195 | ### Using Plugins
196 | 
197 | There is a great collection of third-party [**MkDocs** plugins](https://github.com/mkdocs/mkdocs/wiki/MkDocs-Plugins).
198 | 
199 | Below we shortly describe two that we use to further improve `quizli's` documentation. Feel free to browse the collection for
200 | other plugins that suit your needs.
201 | 
202 | #### [mkdocstrings](https://mkdocstrings.github.io/)
203 | 
204 | With this plugin we can automatically generate documentation from source code.
205 | 
206 | To enable the plugin we add it to our `mkdocs.yml` file:
207 | 
208 | ```yaml
209 | theme:
210 |   name: "material"
211 | 
212 | plugins:
213 |   - search
214 |   {==- mkdocstrings==}
215 | ```
216 | 
217 | Now we can reference `quizli's` source code and it will nicely show up in the documentation.
218 | For example, since we have documented `quizli's` source code properly, the following simple 
219 | reference to it's `quiz` module:
220 | 
221 | ``` yaml title="Example: quiz"
222 | --8<-- "docs/code_reference/quiz.md"
223 | ```
224 | will turn out the following way in the documentation: [Quiz - code reference](../code_reference/quiz.md).
225 | 
226 | You find more advanced usage examples in `mkdocstring`'s [documentation](https://mkdocstrings.github.io/usage/).
227 | 
228 | 
229 | #### [mkdocs-jupyter](https://github.com/danielfrg/mkdocs-jupyter)
230 | 
231 | !!! info "Alternative: `mknotebooks`"
232 |     There is an alternative plugin with a similar feature set:
233 |     [`mknotebooks`](https://github.com/greenape/mknotebooks)
234 | 
235 |     If you want to include jupyter notebooks into your project's documentation
236 |     check out which one fits your needs better. 
237 |     
238 | This plugin allows us to render jupyter notebooks (or Python scripts) in our documentation.
239 | 
240 | After installing it, we can enable the plugin again by simply adding a line to our `mkdocs.yml` file:
241 | 
242 | ``` yaml
243 | plugins:
244 |   - search
245 |   - mkdocstrings
246 |   {==- mkdocs-jupyter==}
247 | ```  
248 | 
249 | To give you an example of how it turns out, take a look at the following [`notebook`](../notebooks/quizli.ipynb).
250 | 


--------------------------------------------------------------------------------
/docs/assets/demo-getting-help.svg:
--------------------------------------------------------------------------------
  1 | 
  2 |     
  3 |         
  4 |             
  5 |             
  6 |         
  7 |         
108 |         
131 |     
132 |     
133 |     
134 |     
135 |     
136 |                                                                                                                                                                                ~/quizli-demo [🐍 pyenv 3.10.0]                                                                                     q                                                                                   quizli --help                                                                       quizli --help                                                                       quizli --help                                                                       quizli --help                                                                       quizli --help                                                                      Usage: quizli [OPTIONS] COMMAND [ARGS]...  Welcome to quizli  Learn to create and open-source an interactive quiz app with Python!  Check out the project at: https://github.com/pwenker/quizli/Options:  --install-completion  Install completion for the current shell.  --show-completion     Show completion for the current shell, to copy it or                        customize the installation.  --help                Show this message and exit.Commands:  demo   Open the documentation page in the browser  start  Start the quiz with the configuration of your choice. quizli --help                                                                       quizli --help                                                                       quizli --help                                                                       quizli --help                                                                       quizli d                                                                            quizli demo                                                                         quizli demo                                                                         quizli demo                                                                         quizli demo                                                                         quizli demo                                                                         quizli demo -                                                                       quizli demo --                                                                      quizli demo --h                                                                     quizli demo --he                                                                    quizli demo --hel                                                                   quizli demo --help                                                                  quizli demo --help                                                                 Usage: quizli demo [OPTIONS]  Open the documentation page in the browser  --help  Show this message and exit. quizli demo --help                                                                  quizli demo --help                                                                  quizli demo --help                                                                  quizli demo --help                                                                  quizli demo --help                                                                  quizli demo --help                                                                  quizli demo --help                                                                  quizli s                                                                            quizli start --quiz-name python_quiz --in-order --mode sudden_death                 quizli start --quiz-name python_quiz --in-order --mode sudden_death                 quizli start --quiz-name python_quiz --in-order --mode sudden_death                 quizli start --quiz-name python_quiz --in-order --mode sudden_death                 quizli start --quiz-name python_quiz --in-order --mode sudden_death                 quizli start --quiz-name python_quiz --in-order --mode sudden_death                 quizli start --quiz-name python_quiz --in-order --mode sudden_death                 quizli start --quiz-name python_quiz --in-order --mode sudden_death                 quizli start --h                                                                    quizli start --help                                                                 quizli start --help                                                                 quizli start --help                                                                 quizli start --help                                                                 quizli start --help                                                                Usage: quizli start [OPTIONS]  Start the quiz with the configuration of your choice.  ### Select Quiz Content  There are 2 ways to create a quiz with `quizli`:  1. From a csv file containing question-answer pairs: e.g. `quizli start  --from-csv examples/quiz.csv` 2. From a function in the `example` module:  e.g. `quizli start --quiz-name python_quiz`  ### Select Quiz Settings  You can choose to  - Shuffle the quiz with the `--randomize` flag (default), or keep it  `--in-order`.  - Terminate the quiz with the first wrong answer with `mode=sudden_death`,  or continue on failure with `mode=complete` (default).  --from-csv PATH                 Read a quiz from a csv-file  --mode [sudden_death|complete]  Select the condition for the quiz to end                                  [default: complete]  --quiz-name [python_quiz|binary_number_quiz]                                  Select a built-in quiz  [default:                                  python_qu                                   python_quiz]  --randomize / --in-order        Shuffle the quiz before starting it                                  [default: True]  --help                          Show this message and exit.                                                                                    
137 | 


--------------------------------------------------------------------------------