├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ ├── feature-request.yml │ └── question.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── continuous-integration.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── NOTICE ├── README.md ├── docs ├── Makefile ├── conf.py ├── images │ ├── autocomplete.gif │ ├── checkbox.gif │ ├── confirm.gif │ ├── example.gif │ ├── password.gif │ ├── path.gif │ ├── print.gif │ ├── rasa-logo.svg │ ├── rawselect.gif │ ├── select.gif │ └── text.gif ├── index.rst └── pages │ ├── advanced.rst │ ├── api_reference.rst │ ├── changelog.rst │ ├── contributors.rst │ ├── installation.rst │ ├── quickstart.rst │ ├── support.rst │ └── types.rst ├── examples ├── __init__.py ├── advanced_workflow.py ├── autocomplete_ants.py ├── checkbox_search.py ├── checkbox_separators.py ├── checkbox_toppings.py ├── confirm_amazed.py ├── confirm_continue.py ├── password_confirm.py ├── password_git.py ├── password_secret.py ├── project_path.py ├── rawselect_action.py ├── rawselect_separator.py ├── readme.py ├── select_action.py ├── select_restaurant.py ├── select_search.py ├── simple_print.py ├── text_name.py └── text_phone_number.py ├── poetry.lock ├── pyproject.toml ├── questionary ├── __init__.py ├── constants.py ├── form.py ├── prompt.py ├── prompts │ ├── __init__.py │ ├── autocomplete.py │ ├── checkbox.py │ ├── common.py │ ├── confirm.py │ ├── password.py │ ├── path.py │ ├── press_any_key_to_continue.py │ ├── rawselect.py │ ├── select.py │ └── text.py ├── py.typed ├── question.py ├── styles.py ├── utils.py └── version.py ├── scripts └── validate_version.py └── tests ├── __init__.py ├── conftest.py ├── prompts ├── __init__.py ├── test_autocomplete.py ├── test_checkbox.py ├── test_common.py ├── test_confirm.py ├── test_password.py ├── test_path.py ├── test_press_any_key_to_continue.py ├── test_rawselect.py ├── test_select.py └── test_text.py ├── test_examples.py ├── test_form.py ├── test_prompt.py ├── test_question.py ├── test_utils.py └── utils.py /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.208.0/containers/python-3/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster 4 | ARG VARIANT="3.9-bullseye" 5 | 6 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} 7 | 8 | ENV PYTHONFAULTHANDLER=1 \ 9 | PYTHONUNBUFFERED=1 \ 10 | PYTHONHASHSEED=random \ 11 | PIP_NO_CACHE_DIR=off \ 12 | PIP_DISABLE_PIP_VERSION_CHECK=on \ 13 | PIP_DEFAULT_TIMEOUT=100 14 | 15 | # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. 16 | # COPY requirements.txt /tmp/pip-tmp/ 17 | # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ 18 | # && rm -rf /tmp/pip-tmp 19 | RUN pip install 'poetry==1.1.8' 20 | COPY poetry.lock pyproject.toml ./ 21 | RUN poetry config virtualenvs.create false \ 22 | && poetry install --no-interaction --no-ansi --extras "docs" 23 | 24 | 25 | # [Optional] Uncomment this section to install additional OS packages. 26 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 27 | # && apt-get -y install --no-install-recommends 28 | 29 | # [Optional] Uncomment this line to install global node packages. 30 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 31 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.208.0/containers/python-3 3 | { 4 | "name": "Python 3", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "context": "..", 8 | "args": { 9 | // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6 10 | // Append -bullseye or -buster to pin to an OS version. 11 | // Use -bullseye variants on local on arm64/Apple Silicon. 12 | "VARIANT": "3.8" 13 | } 14 | }, 15 | 16 | // Set *default* container specific settings.json values on container create. 17 | "settings": { 18 | "python.defaultInterpreterPath": "/usr/local/bin/python", 19 | "python.linting.enabled": true, 20 | "python.linting.pylintEnabled": true, 21 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", 22 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black", 23 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", 24 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", 25 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", 26 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", 27 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", 28 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" 29 | }, 30 | 31 | // Add the IDs of extensions you want installed when the container is created. 32 | "extensions": [ 33 | "ms-python.python", 34 | "ms-python.vscode-pylance" 35 | ], 36 | 37 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 38 | // "forwardPorts": [], 39 | 40 | // Use 'postCreateCommand' to run commands after the container is created. 41 | // "postCreateCommand": "pip3 install --user -r requirements.txt", 42 | 43 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 44 | "remoteUser": "vscode", 45 | "features": { 46 | "docker-in-docker": "20.10", 47 | "docker-from-docker": "20.10", 48 | "kubectl-helm-minikube": "1.22", 49 | "terraform": "1.0", 50 | "git": "os-provided", 51 | "github-cli": "latest", 52 | "sshd": "latest" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report to help us improve 3 | labels: [Bug] 4 | 5 | body: 6 | - type: textarea 7 | id: describe-bug 8 | attributes: 9 | label: Describe the bug 10 | description: > 11 | Please give us a clear and concise description of what the bug is. 12 | Please include a screenshot if you are able to. 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | id: example 18 | attributes: 19 | label: Example 20 | description: > 21 | Please provide a small and concise example to reproduce the issue. 22 | This will be automatically formatted into code, so no need for 23 | backticks. 24 | render: python 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: steps 30 | attributes: 31 | label: Steps to reproduce 32 | description: Please provide any further steps to reproduce the issue. 33 | placeholder: | 34 | 1. ... 35 | 2. ... 36 | 3. ... 37 | 38 | - type: textarea 39 | id: expected-outcome 40 | attributes: 41 | label: Expected behaviour 42 | description: Please tell us what the expected behaviour should be been. 43 | validations: 44 | required: true 45 | 46 | - type: checkboxes 47 | id: checked-latest 48 | attributes: 49 | label: Latest version 50 | description: > 51 | Please ensure that you have checked that this issue occurs on the 52 | latest version of questionary. You can upgrade by running 53 | `pip install -U questionary`. 54 | options: 55 | - label: > 56 | I have checked that this issue occurs on the latest version of 57 | questionary. 58 | required: true 59 | 60 | - type: input 61 | id: questionary-version 62 | attributes: 63 | label: Questionary version 64 | description: > 65 | You can run `pip show questionary` to see the version of questionary 66 | that is installed. 67 | validations: 68 | required: true 69 | 70 | - type: input 71 | id: prompt-toolkit-version 72 | attributes: 73 | label: Prompt Toolkit version 74 | description: > 75 | You can run `pip show prompt_toolkit` to see the version of Prompt 76 | Toolkit that is installed. 77 | validations: 78 | required: true 79 | 80 | - type: dropdown 81 | id: operating-system 82 | attributes: 83 | label: Operating System 84 | description: Which operating system are you using? 85 | options: 86 | - Windows 87 | - macOS 88 | - Linux 89 | - Other (please specify in description) 90 | validations: 91 | required: true 92 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Documentation 5 | url: https://questionary.readthedocs.io/ 6 | about: Please find answers to common questions here 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | labels: [Enhancement] 4 | 5 | body: 6 | - type: textarea 7 | id: problem-description 8 | attributes: 9 | label: Describe the problem 10 | description: > 11 | Is your feature request related to a problem? Please provide a clear 12 | and concise description of what the problem is. 13 | placeholder: I'm always frustrated when... 14 | validations: 15 | required: true 16 | 17 | - type: textarea 18 | id: solution-description 19 | attributes: 20 | label: Describe the solution 21 | description: > 22 | Please provide a clear and concise description of the solution that you 23 | would like to see. 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | id: alternatives 29 | attributes: 30 | label: Alternatives considered 31 | description: > 32 | Please provide a clear and concise description of any alternative 33 | solutions or features you have considered. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: Need help 2 | description: Ask a question if you need help 3 | labels: [Question] 4 | 5 | body: 6 | - type: textarea 7 | id: question 8 | attributes: 9 | label: Question 10 | description: > 11 | Please describe the problem that you are having. 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | id: previous-attempts 17 | attributes: 18 | label: What have you already tried? 19 | description: > 20 | Please describe any attempts that you have made to solve the problem. 21 | This might include code examples. This will help us get a better 22 | understanding of what you are trying to do. 23 | validations: 24 | required: true 25 | 26 | - type: checkboxes 27 | id: read-the-docs 28 | attributes: 29 | label: Read the documentation 30 | description: > 31 | Please check if your question is answered by the 32 | [documentation](https://questionary.readthedocs.io/). Don't worry if 33 | you ask a question that is already answered by the documentation - we'll 34 | just point you to the right place. 35 | options: 36 | - label: > 37 | I have checked to ensure that my question is not answered by the 38 | documentation. 39 | required: true 40 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **What is the problem that this PR addresses?** 2 | 3 | 4 | 5 | ... 6 | 7 | **How did you solve it?** 8 | 9 | 10 | ... 11 | 12 | **Checklist** 13 | 14 | 15 | 16 | 17 | - [ ] I have read the [Contributor's Guide](https://questionary.readthedocs.io/en/stable/pages/contributors.html#steps-for-submitting-code). 18 | - [ ] I will check that all automated PR checks pass before the PR gets reviewed. 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "pip" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "*" 9 | pull_request: 10 | 11 | # SECRETS 12 | # - PYPI_TOKEN: publishing token for tmbo account, needs to be maintainer of 13 | # tmbo/questionary on pypi 14 | 15 | jobs: 16 | quality: 17 | name: Code Quality 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout git repository 🕝 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up Python 3.12 🐍 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.12" 28 | 29 | - name: Install poetry 🦄 30 | uses: Gr1N/setup-poetry@v9 31 | 32 | - name: Load Poetry Cached Libraries ⬇ 33 | uses: actions/cache@v4 34 | with: 35 | path: ~/.cache/pypoetry 36 | key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} 37 | restore-keys: ${{ runner.os }}-poetry- 38 | 39 | - name: Install dependencies 🖥 40 | run: poetry install --no-interaction 41 | 42 | - name: Lint Code 🎎 43 | run: make lint 44 | 45 | - name: Check Types 📚 46 | run: make types 47 | 48 | - name: Check Version Numbers 🕸 49 | run: poetry run python scripts/validate_version.py 50 | 51 | test: 52 | name: Run Tests 53 | runs-on: ${{ matrix.os }} 54 | timeout-minutes: 10 55 | strategy: 56 | matrix: 57 | os: [ubuntu-latest, windows-latest, macos-latest] 58 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 59 | promttoolkit: [3.*, 2.*] 60 | include: 61 | - promttoolkit: 3.0.29 62 | os: ubuntu-latest 63 | - promttoolkit: 3.0.19 64 | os: ubuntu-latest 65 | 66 | steps: 67 | - name: Checkout git repository 🕝 68 | uses: actions/checkout@v4 69 | 70 | - name: Set up Python ${{ matrix.python-version }} 🐍 71 | uses: actions/setup-python@v5 72 | with: 73 | python-version: ${{ matrix.python-version }} 74 | 75 | - name: Install poetry 🦄 76 | uses: Gr1N/setup-poetry@v9 77 | 78 | - name: Load Poetry Cached Libraries ⬇ 79 | uses: actions/cache@v4 80 | with: 81 | path: ~/.cache/pypoetry 82 | key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} 83 | restore-keys: ${{ runner.os }}-poetry- 84 | 85 | - name: Install dependencies 🖥 86 | run: | 87 | poetry install --no-interaction 88 | poetry run pip install prompt_toolkit==${{ matrix.promttoolkit }} 89 | 90 | - name: Test Code 🔍 91 | run: make test 92 | 93 | - name: Send Coverage Report 📊 94 | env: 95 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 96 | COVERALLS_SERVICE_NAME: github 97 | COVERALLS_PARALLEL: true 98 | run: poetry run coveralls 99 | post-test: 100 | name: Signal test completion 101 | needs: test 102 | runs-on: ubuntu-latest 103 | container: python:3-slim 104 | steps: 105 | - name: Finished 106 | run: | 107 | pip3 install --upgrade coveralls 108 | coveralls --service=github --finish 109 | env: 110 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 111 | 112 | docs: 113 | name: Test Docs 114 | runs-on: ubuntu-latest 115 | 116 | steps: 117 | - name: Checkout git repository 🕝 118 | uses: actions/checkout@v4 119 | 120 | - name: Set up Python 3.12 🐍 121 | uses: actions/setup-python@v5 122 | with: 123 | python-version: "3.12" 124 | 125 | - name: Install poetry 🦄 126 | uses: Gr1N/setup-poetry@v9 127 | 128 | - name: Load Poetry Cached Libraries ⬇ 129 | uses: actions/cache@v4 130 | with: 131 | path: ~/.cache/pypoetry 132 | key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} 133 | restore-keys: ${{ runner.os }}-poetry 134 | 135 | - name: Install dependencies 🖥 136 | run: poetry install --no-interaction --with=docs 137 | 138 | - name: Build docs ⚒️ 139 | run: make docs 140 | 141 | deploy: 142 | name: Deploy to PyPI 143 | runs-on: ubuntu-latest 144 | 145 | # deploy will only be run when there is a tag available 146 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 147 | needs: [quality, test, docs] # only run after all other stages succeeded 148 | 149 | steps: 150 | - name: Checkout git repository 🕝 151 | uses: actions/checkout@v4 152 | 153 | - name: Set up Python 3.12 🐍 154 | uses: actions/setup-python@v5 155 | with: 156 | python-version: "3.12" 157 | 158 | - name: Install poetry 🦄 159 | uses: Gr1N/setup-poetry@v9 160 | 161 | - name: Build ⚒️ Distributions 162 | run: | 163 | poetry build 164 | poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }} 165 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *# 2 | *.DS_Store 3 | *.egg 4 | *.eggs 5 | *.egg-info 6 | *.egg-info/ 7 | *.iml 8 | *.log 9 | *.pyc 10 | *.sass-cache 11 | *.sqlite 12 | *build/ 13 | *dat 14 | *npy 15 | *pyc 16 | *~ 17 | .env 18 | .cache/ 19 | .pytest_cache/ 20 | .coverage 21 | .idea/ 22 | .vscode/ 23 | .ipynb_checkpoints 24 | .ruby-version 25 | .tox 26 | bower_components/ 27 | build/ 28 | build/lib/ 29 | dist/ 30 | docs/_build 31 | jnk/ 32 | logs/ 33 | profile.* 34 | server/ 35 | tmp/ 36 | .python-version 37 | .venv -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/PyCQA/autoflake 3 | rev: v2.3.1 4 | hooks: 5 | - id: autoflake 6 | args: ["-i", "--remove-all-unused-imports"] 7 | 8 | - repo: https://github.com/psf/black 9 | rev: 23.3.0 10 | hooks: 11 | - id: black 12 | args: 13 | - "--target-version=py38" 14 | - "--target-version=py39" 15 | - "--target-version=py310" 16 | - "--target-version=py311" 17 | - "--line-length=88" 18 | 19 | - repo: https://github.com/pycqa/isort 20 | rev: 5.12.0 21 | hooks: 22 | - id: isort 23 | name: isort (python) 24 | args: 25 | - "--force-single-line-imports" 26 | - "--profile=black" 27 | 28 | - repo: https://github.com/pycqa/flake8 29 | rev: 7.1.0 30 | hooks: 31 | - id: flake8 32 | args: ["--max-line-length", "120", "--max-doc-length", "140"] 33 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | formats: 4 | - pdf 5 | 6 | sphinx: 7 | configuration: docs/conf.py 8 | fail_on_warning: true 9 | 10 | build: 11 | os: "ubuntu-22.04" 12 | tools: 13 | python: "3.11" 14 | jobs: 15 | post_create_environment: 16 | - pip install poetry 17 | - poetry config virtualenvs.create false 18 | post_install: 19 | - make install 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Tom Bocklisch and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE NOTICE README.md requirements.txt 2 | include pyproject.toml 3 | include questionary/py.typed 4 | recursive-include docs *.gif 5 | recursive-include docs *.png 6 | recursive-include examples *.py 7 | recursive-include tests *.py 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean install develop lint test types docs livedocs 2 | 3 | JOBS ?= 1 4 | 5 | help: 6 | @echo "make" 7 | @echo " clean" 8 | @echo " Remove Python/build artifacts." 9 | @echo " develop" 10 | @echo " Configure development environment for questionary." 11 | @echo " install" 12 | @echo " Install questionary." 13 | @echo " lint" 14 | @echo " Check the code style and apply black formatting." 15 | @echo " test" 16 | @echo " Run the unit tests." 17 | @echo " types" 18 | @echo " Check for type errors using pytype." 19 | @echo " docs" 20 | @echo " Build the documentation." 21 | @echo " livedocs" 22 | @echo " Build the documentation with a live preview for quick iteration." 23 | 24 | clean: 25 | find . -name '*.pyc' -exec rm -f {} + 26 | find . -name '*.pyo' -exec rm -f {} + 27 | find . -name '*~' -exec rm -f {} + 28 | rm -rf build/ 29 | rm -rf questionary.egg-info/ 30 | rm -rf .mypy_cache/ 31 | rm -rf .pytest_cache/ 32 | rm -rf dist/ 33 | poetry run make -C docs clean 34 | 35 | install: 36 | poetry install --with="docs" 37 | 38 | develop: install 39 | poetry run pre-commit install 40 | 41 | lint: 42 | poetry run pre-commit run -a 43 | 44 | test: 45 | poetry run pytest --cov questionary -v 46 | 47 | types: 48 | poetry run mypy --version 49 | poetry run mypy questionary 50 | 51 | docs: 52 | poetry run make -C docs html 53 | 54 | livedocs: 55 | poetry run sphinx-autobuild docs docs/build/html 56 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Tom Bocklisch 2 | Copyright 2019 Tom Bocklisch 3 | 4 | ---- 5 | 6 | This product includes software from PyInquirer (https://github.com/CITGuru/PyInquirer), 7 | under the MIT License. 8 | 9 | Copyright 2018 Oyetoke Toby and contributors 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy of 12 | this software and associated documentation files (the "Software"), to deal in 13 | the Software without restriction, including without limitation the rights to 14 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 15 | of the Software, and to permit persons to whom the Software is furnished to do 16 | so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | 29 | ---- 30 | 31 | This product includes software from whaaaaat (https://github.com/finklabs/whaaaaat), 32 | under the MIT License. 33 | 34 | Copyright 2016 Fink Labs GmbH and inquirerpy contributors 35 | 36 | Permission is hereby granted, free of charge, to any person obtaining a copy of 37 | this software and associated documentation files (the "Software"), to deal in 38 | the Software without restriction, including without limitation the rights to 39 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 40 | of the Software, and to permit persons to whom the Software is furnished to do 41 | so, subject to the following conditions: 42 | 43 | The above copyright notice and this permission notice shall be included in all 44 | copies or substantial portions of the Software. 45 | 46 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 47 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 48 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 49 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 50 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 51 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Questionary 2 | 3 | [![Version](https://img.shields.io/pypi/v/questionary.svg)](https://pypi.org/project/questionary/) 4 | [![License](https://img.shields.io/pypi/l/questionary.svg)](#) 5 | [![Continuous Integration](https://github.com/tmbo/questionary/workflows/Continuous%20Integration/badge.svg)](#) 6 | [![Coverage](https://coveralls.io/repos/github/tmbo/questionary/badge.svg?branch=master)](https://coveralls.io/github/tmbo/questionary?branch=master) 7 | [![Supported Python Versions](https://img.shields.io/pypi/pyversions/questionary.svg)](https://pypi.python.org/pypi/questionary) 8 | [![Documentation](https://readthedocs.org/projects/questionary/badge/?version=latest)](https://questionary.readthedocs.io/en/latest/?badge=latest) 9 | 10 | ✨ Questionary is a Python library for effortlessly building pretty command line interfaces ✨ 11 | 12 | * [Features](#features) 13 | * [Installation](#installation) 14 | * [Usage](#usage) 15 | * [Documentation](#documentation) 16 | * [Support](#support) 17 | 18 | 19 | ![Example](https://raw.githubusercontent.com/tmbo/questionary/master/docs/images/example.gif) 20 | 21 | ```python3 22 | import questionary 23 | 24 | questionary.text("What's your first name").ask() 25 | questionary.password("What's your secret?").ask() 26 | questionary.confirm("Are you amazed?").ask() 27 | 28 | questionary.select( 29 | "What do you want to do?", 30 | choices=["Order a pizza", "Make a reservation", "Ask for opening hours"], 31 | ).ask() 32 | 33 | questionary.rawselect( 34 | "What do you want to do?", 35 | choices=["Order a pizza", "Make a reservation", "Ask for opening hours"], 36 | ).ask() 37 | 38 | questionary.checkbox( 39 | "Select toppings", choices=["foo", "bar", "bazz"] 40 | ).ask() 41 | 42 | questionary.path("Path to the projects version file").ask() 43 | ``` 44 | 45 | Used and supported by 46 | 47 | [](https://github.com/RasaHQ/rasa) 48 | 49 | ## Features 50 | 51 | Questionary supports the following input prompts: 52 | 53 | * [Text](https://questionary.readthedocs.io/en/stable/pages/types.html#text) 54 | * [Password](https://questionary.readthedocs.io/en/stable/pages/types.html#password) 55 | * [File Path](https://questionary.readthedocs.io/en/stable/pages/types.html#file-path) 56 | * [Confirmation](https://questionary.readthedocs.io/en/stable/pages/types.html#confirmation) 57 | * [Select](https://questionary.readthedocs.io/en/stable/pages/types.html#select) 58 | * [Raw select](https://questionary.readthedocs.io/en/stable/pages/types.html#raw-select) 59 | * [Checkbox](https://questionary.readthedocs.io/en/stable/pages/types.html#checkbox) 60 | * [Autocomplete](https://questionary.readthedocs.io/en/stable/pages/types.html#autocomplete) 61 | 62 | There is also a helper to [print formatted text](https://questionary.readthedocs.io/en/stable/pages/types.html#printing-formatted-text) 63 | for when you want to spice up your printed messages a bit. 64 | 65 | ## Installation 66 | 67 | Use the package manager [pip](https://pip.pypa.io/en/stable/) to install Questionary: 68 | 69 | ```bash 70 | pip install questionary 71 | ``` 72 | ✨🎂✨ 73 | 74 | ## Usage 75 | 76 | ```python 77 | import questionary 78 | 79 | questionary.select( 80 | "What do you want to do?", 81 | choices=[ 82 | 'Order a pizza', 83 | 'Make a reservation', 84 | 'Ask for opening hours' 85 | ]).ask() # returns value of selection 86 | ``` 87 | 88 | That's all it takes to create a prompt! Have a [look at the documentation](https://questionary.readthedocs.io/) 89 | for some more examples. 90 | 91 | ## Documentation 92 | 93 | Documentation for Questionary is available [here](https://questionary.readthedocs.io/). 94 | 95 | ## Support 96 | 97 | Please [open an issue](https://github.com/tmbo/questionary/issues/new) 98 | with enough information for us to reproduce your problem. 99 | A [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) 100 | would be very helpful. 101 | 102 | ## Contributing 103 | 104 | Contributions are very much welcomed and appreciated. Head over to the documentation on [how to contribute](https://questionary.readthedocs.io/en/stable/pages/contributors.html#steps-for-submitting-code). 105 | 106 | ## Authors and Acknowledgment 107 | 108 | Questionary is written and maintained by Tom Bocklisch and Kian Cross. 109 | 110 | It is based on the great work by [Oyetoke Toby](https://github.com/CITGuru/PyInquirer) 111 | and [Mark Fink](https://github.com/finklabs/whaaaaat). 112 | 113 | ## License 114 | Licensed under the [MIT License](https://github.com/tmbo/questionary/blob/master/LICENSE). Copyright 2021 Tom Bocklisch. 115 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -W --keep-going -n 6 | SPHINXBUILD = python -m sphinx 7 | SOURCEDIR = . 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 20 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath("../")) 5 | from questionary import __version__ # noqa: E402 6 | 7 | project = "Questionary" 8 | copyright = "2021, Questionary" 9 | author = "Questionary" 10 | 11 | version = __version__ 12 | release = __version__ 13 | 14 | extensions = [ 15 | "sphinx.ext.autodoc", 16 | "sphinx.ext.intersphinx", 17 | "sphinx.ext.viewcode", 18 | "sphinx.ext.napoleon", 19 | "sphinx_copybutton", 20 | "sphinx_autodoc_typehints", 21 | ] 22 | 23 | autodoc_typehints = "description" 24 | 25 | copybutton_prompt_text = r">>> |\.\.\. |\$ " 26 | copybutton_prompt_is_regexp = True 27 | 28 | html_theme = "sphinx_rtd_theme" 29 | 30 | html_theme_options = { 31 | "navigation_depth": 2, 32 | } 33 | 34 | intersphinx_mapping = { 35 | "python": ("https://docs.python.org/3", None), 36 | "prompt_toolkit": ("https://python-prompt-toolkit.readthedocs.io/en/3.0.36/", None), 37 | } 38 | 39 | autodoc_member_order = "alphabetical" 40 | -------------------------------------------------------------------------------- /docs/images/autocomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/docs/images/autocomplete.gif -------------------------------------------------------------------------------- /docs/images/checkbox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/docs/images/checkbox.gif -------------------------------------------------------------------------------- /docs/images/confirm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/docs/images/confirm.gif -------------------------------------------------------------------------------- /docs/images/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/docs/images/example.gif -------------------------------------------------------------------------------- /docs/images/password.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/docs/images/password.gif -------------------------------------------------------------------------------- /docs/images/path.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/docs/images/path.gif -------------------------------------------------------------------------------- /docs/images/print.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/docs/images/print.gif -------------------------------------------------------------------------------- /docs/images/rasa-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/rawselect.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/docs/images/rawselect.gif -------------------------------------------------------------------------------- /docs/images/select.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/docs/images/select.gif -------------------------------------------------------------------------------- /docs/images/text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/docs/images/text.gif -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | *********** 2 | Questionary 3 | *********** 4 | 5 | .. image:: https://img.shields.io/pypi/v/questionary.svg 6 | :target: https://pypi.org/project/questionary/ 7 | :alt: Version 8 | 9 | .. image:: https://img.shields.io/pypi/l/questionary.svg 10 | :target: # 11 | :alt: License 12 | 13 | .. image:: https://img.shields.io/pypi/pyversions/questionary.svg 14 | :target: https://pypi.python.org/pypi/questionary 15 | :alt: Supported Python Versions 16 | 17 | ✨ Questionary is a Python library for effortlessly building pretty command line interfaces ✨ 18 | 19 | It makes it very easy to query your user for input. You need your user to 20 | confirm a destructive action or enter a file path? We've got you covered: 21 | 22 | .. image:: images/example.gif 23 | 24 | Creating your first prompt is just a few key strokes away 25 | 26 | .. code-block:: python3 27 | 28 | import questionary 29 | 30 | first_name = questionary.text("What's your first name").ask() 31 | 32 | This prompt will ask the user to provide free text input and the result is 33 | stored in ``first_name``. 34 | 35 | You can install Questionary using pip (for details, head over to the :ref:`installation` page): 36 | 37 | .. code-block:: console 38 | 39 | $ pip install questionary 40 | 41 | Ready to go? Check out the :ref:`quickstart`. 42 | 43 | License 44 | ======= 45 | Licensed under the `MIT License `_. 46 | Copyright 2020 Tom Bocklisch. 47 | 48 | .. toctree:: 49 | :hidden: 50 | 51 | pages/installation 52 | pages/quickstart 53 | pages/types 54 | pages/advanced 55 | pages/api_reference 56 | pages/support 57 | Examples 58 | 59 | .. toctree:: 60 | :hidden: 61 | 62 | pages/contributors 63 | 64 | .. toctree:: 65 | :hidden: 66 | 67 | pages/changelog 68 | -------------------------------------------------------------------------------- /docs/pages/advanced.rst: -------------------------------------------------------------------------------- 1 | ***************** 2 | Advanced Concepts 3 | ***************** 4 | 5 | This page describes some of the more advanced uses of Questionary. 6 | 7 | Validation 8 | ########## 9 | 10 | Many of the prompts support a ``validate`` argument, which allows 11 | the answer to be validated before being submitted. A user can not 12 | submit an answer if it doesn't pass the validation. 13 | 14 | The example below shows :meth:`~questionary.text` input with 15 | a validation: 16 | 17 | .. code-block:: python3 18 | 19 | import questionary 20 | from questionary import Validator, ValidationError, prompt 21 | 22 | class NameValidator(Validator): 23 | def validate(self, document): 24 | if len(document.text) == 0: 25 | raise ValidationError( 26 | message="Please enter a value", 27 | cursor_position=len(document.text), 28 | ) 29 | 30 | questionary.text("What's your name?", validate=NameValidator).ask() 31 | 32 | In this example, the user can not enter a non empty value. If the 33 | prompt is submitted without a value. Questionary will show the error 34 | message and reject the submission until the user enters a value. 35 | 36 | Alternatively, we can replace the ``NameValidator`` class with a simple 37 | function, as seen below: 38 | 39 | .. code-block:: python3 40 | 41 | import questionary 42 | 43 | print(questionary.text( 44 | "What's your name?", 45 | validate=lambda text: True if len(text) > 0 else "Please enter a value" 46 | ).ask()) 47 | 48 | Finally, if we do not care about the error message being displayed, we can omit 49 | the error message from the final example to use the default: 50 | 51 | .. code-block:: python3 52 | 53 | import questionary 54 | 55 | print(questionary.text("What's your name?", validate=lambda text: len(text) > 0).ask()) 56 | 57 | .. admonition:: example 58 | :class: info 59 | 60 | The :meth:`~questionary.checkbox` prompt does not support passing a 61 | ``Validator``. See the :ref:`API Reference ` for all the prompts which 62 | support the ``validate`` parameter. 63 | 64 | A Validation Example using the Password Question 65 | ************************************************ 66 | 67 | Here we see an example of ``validate`` being used on a 68 | :meth:`~questionary.password` prompt to enforce complexity requirements: 69 | 70 | .. code-block:: python3 71 | 72 | import re 73 | import questionary 74 | 75 | def password_validator(password): 76 | 77 | if len(password) < 10: 78 | return "Password must be at least 10 characters" 79 | 80 | elif re.search("[0-9]", password) is None: 81 | return "Password must contain a number" 82 | 83 | elif re.search("[a-z]", password) is None: 84 | return "Password must contain an lower-case letter" 85 | 86 | elif re.search("[A-Z]", password) is None: 87 | return "Password must contain an upper-case letter" 88 | 89 | else: 90 | return True 91 | 92 | print(questionary.password("Enter your password", validate=password_validator).ask()) 93 | 94 | Keyboard Interrupts 95 | ################### 96 | 97 | Prompts can be invoked in either a 'safe' or 'unsafe' way. The safe way 98 | captures keyboard interrupts and handles them by catching the interrupt 99 | and returning ``None`` for the asked question. If a question is asked 100 | using unsafe functions, the keyboard interrupts are not caught. 101 | 102 | Safe 103 | **** 104 | 105 | The following are safe (capture keyboard interrupts): 106 | 107 | * :meth:`~questionary.prompt`; 108 | 109 | * :attr:`~questionary.Form.ask` on :class:`~questionary.Form` (returned by 110 | :meth:`~questionary.form`); 111 | 112 | * :attr:`~questionary.Question.ask` on :class:`~questionary.Question`, which 113 | is returned by the various prompt functions (e.g. :meth:`~questionary.text`, 114 | :meth:`~questionary.checkbox`). 115 | 116 | When a keyboard interrupt is captured, the message ``"Cancelled by user"`` is 117 | displayed (or a custom message, if one is given) and ``None`` is returned. 118 | Here is an example: 119 | 120 | .. code:: python3 121 | 122 | # Questionary handles keyboard interrupt and returns `None` if the 123 | # user hits e.g. `Ctrl+C` 124 | prompt(...) 125 | 126 | Unsafe 127 | ****** 128 | 129 | The following are unsafe (do not catch keyboard interrupts): 130 | 131 | * :meth:`~questionary.unsafe_prompt`; 132 | 133 | * :attr:`~questionary.Form.unsafe_ask` on :class:`~questionary.Form` (returned by 134 | :meth:`~questionary.form`); 135 | 136 | * :attr:`~questionary.Question.unsafe_ask` on :class:`~questionary.Question`, 137 | which is returned by the various prompt functions (e.g. :meth:`~questionary.text`, 138 | :meth:`~questionary.checkbox`). 139 | 140 | As a caller you must handle keyboard interrupts yourself 141 | when calling these methods. Here is an example: 142 | 143 | .. code:: python3 144 | 145 | try: 146 | unsafe_prompt(...) 147 | 148 | except KeyboardInterrupt: 149 | # your chance to handle the keyboard interrupt 150 | print("Cancelled by user") 151 | 152 | 153 | Asynchronous Usage 154 | ################## 155 | 156 | If you are running asynchronous code and you want to avoid blocking your 157 | async loop, you can ask your questions using ``await``. 158 | :class:`questionary.Question` and :class:`questionary.Form` have 159 | ``ask_async`` and ``unsafe_ask_async`` methods to invoke the 160 | question using :mod:`python:asyncio`: 161 | 162 | .. code-block:: python3 163 | 164 | import questionary 165 | 166 | answer = await questionary.text("What's your name?").ask_async() 167 | 168 | Themes & Styling 169 | ################ 170 | 171 | You can customize all the colors used for the prompts. Every part of the prompt 172 | has an identifier, which you can use to style it. Let's create your own custom 173 | style: 174 | 175 | .. code-block:: python3 176 | 177 | from questionary import Style 178 | 179 | custom_style_fancy = Style([ 180 | ('qmark', 'fg:#673ab7 bold'), # token in front of the question 181 | ('question', 'bold'), # question text 182 | ('answer', 'fg:#f44336 bold'), # submitted answer text behind the question 183 | ('pointer', 'fg:#673ab7 bold'), # pointer used in select and checkbox prompts 184 | ('highlighted', 'fg:#673ab7 bold'), # pointed-at choice in select and checkbox prompts 185 | ('selected', 'fg:#cc5454'), # style for a selected item of a checkbox 186 | ('separator', 'fg:#cc5454'), # separator in lists 187 | ('instruction', ''), # user instructions for select, rawselect, checkbox 188 | ('text', ''), # plain text 189 | ('disabled', 'fg:#858585 italic') # disabled choices for select and checkbox prompts 190 | ]) 191 | 192 | To use the custom style, you need to pass it to the question as a parameter: 193 | 194 | .. code-block:: python3 195 | 196 | questionary.text("What's your phone number", style=custom_style_fancy).ask() 197 | 198 | .. note:: 199 | 200 | Default values will be used for any token types not specified in your custom style. 201 | 202 | Styling Choices in Select & Checkbox Questions 203 | ********************************************** 204 | 205 | It is also possible to use a list of token tuples as a ``Choice`` title to 206 | change how an option is displayed in :class:`questionary.select` and 207 | :class:`questionary.checkbox`. Make sure to define any additional styles 208 | as part of your custom style definition. 209 | 210 | .. code-block:: python3 211 | 212 | import questionary 213 | from questionary import Choice, Style 214 | 215 | custom_style_fancy = questionary.Style([ 216 | ("highlighted", "bold"), # style for a token which should appear highlighted 217 | ]) 218 | 219 | choices = [Choice(title=[("class:text", "order a "), 220 | ("class:highlighted", "big pizza")])] 221 | 222 | questionary.select( 223 | "What do you want to do?", 224 | choices=choices, 225 | style=custom_style_fancy).ask() 226 | 227 | Conditionally Skip Questions 228 | ############################ 229 | 230 | Sometimes it is helpful to be able to skip a question based on a condition. 231 | To avoid the need for an ``if`` around the question, you can pass the 232 | condition when you create the question: 233 | 234 | .. code-block:: python3 235 | 236 | import questionary 237 | 238 | DISABLED = True 239 | response = questionary.confirm("Are you amazed?").skip_if(DISABLED, default=True).ask() 240 | 241 | If the condition (in this case ``DISABLED``) is ``True``, the question 242 | will be skipped and the default value gets returned, otherwise the user will 243 | be prompted as usual and the default value will be ignored. 244 | 245 | .. _question_dictionaries: 246 | 247 | Create Questions from Dictionaries 248 | ################################## 249 | 250 | Instead of creating questions using the Python functions, you can also create 251 | them using a configuration dictionary: 252 | 253 | .. code-block:: python3 254 | 255 | from questionary import prompt 256 | 257 | questions = [ 258 | { 259 | 'type': 'text', 260 | 'name': 'phone', 261 | 'message': "What's your phone number", 262 | }, 263 | { 264 | 'type': 'confirm', 265 | 'message': 'Do you want to continue?', 266 | 'name': 'continue', 267 | 'default': True, 268 | } 269 | ] 270 | 271 | answers = prompt(questions) 272 | 273 | The questions will be prompted one after another and ``prompt`` will return 274 | as soon as all of them are answered. The returned ``answers`` 275 | will be a dictionary containing the responses, e.g. 276 | 277 | .. code-block:: python3 278 | 279 | {"phone": "0123123", "continue": False}. 280 | 281 | Each configuration dictionary for a question must contain the 282 | following keys: 283 | 284 | ``type`` (required) 285 | The type of the question. 286 | 287 | ``name`` (required) 288 | The name of the question (will be used as key in the 289 | ``answers`` dictionary). 290 | 291 | ``message`` (required) 292 | Message that will be shown to the user. 293 | 294 | In addition to these required configuration parameters, you can 295 | add the following optional parameters: 296 | 297 | ``qmark`` (optional) 298 | Question mark to use - defaults to ``?``. 299 | 300 | ``default`` (optional) 301 | Preselected value. 302 | 303 | ``choices`` (optional) 304 | List of choices (applies when ``'type': 'select'``) 305 | or function returning a list of choices. 306 | 307 | ``when`` (optional) 308 | Function checking if this question should be shown 309 | or skipped (same functionality as :attr:`~questionary.Question.skip_if`). 310 | 311 | ``validate`` (optional) 312 | Function or Validator Class performing validation (will 313 | be performed in real time as users type). 314 | 315 | ``filter`` (optional) 316 | Receive the user input and return the filtered value to be 317 | used inside the program. 318 | 319 | Further information can be found at the :class:`questionary.prompt` 320 | documentation. 321 | 322 | .. _random_label: 323 | 324 | A Complex Example using a Dictionary Configuration 325 | ************************************************** 326 | 327 | Questionary allows creating quite complex workflows when combining all of the 328 | above concepts: 329 | 330 | .. literalinclude:: ../../examples/advanced_workflow.py 331 | :language: python3 332 | 333 | 334 | The above workflow will show to the user the following prompts: 335 | 336 | 1. Yes/No question ``"Would you like the next question?"``. 337 | 338 | 2. ``"Name this library?"`` - only shown when the first question is answered 339 | with yes. 340 | 341 | 3. A question to select an item from a list. 342 | 4. Free text input if ``"other"`` is selected in step 3. 343 | 344 | Depending on the route the user took, the result will look like the following: 345 | 346 | .. code-block:: python3 347 | 348 | { 349 | 'conditional_step': False, 350 | 'second_question': 'Test input' # Free form text 351 | } 352 | 353 | .. code-block:: python3 354 | 355 | { 356 | 'conditional_step': True, 357 | 'next_question': 'questionary', 358 | 'second_question': 'Test input' # Free form text 359 | } 360 | 361 | You can test this workflow yourself by running the 362 | `advanced_workflow.py example `_. 363 | -------------------------------------------------------------------------------- /docs/pages/api_reference.rst: -------------------------------------------------------------------------------- 1 | .. _api-reference: 2 | 3 | ************* 4 | API Reference 5 | ************* 6 | 7 | .. autoclass:: questionary::Choice 8 | :members: 9 | 10 | .. autoclass:: questionary::Form 11 | :members: 12 | 13 | .. autoclass:: questionary::Question 14 | :members: 15 | 16 | .. autoclass:: questionary::Separator 17 | :members: 18 | 19 | .. autoclass:: questionary::FormField 20 | :members: 21 | 22 | .. automethod:: questionary::form 23 | 24 | .. automethod:: questionary::prompt 25 | 26 | .. automethod:: questionary::unsafe_prompt 27 | -------------------------------------------------------------------------------- /docs/pages/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | ********* 4 | Changelog 5 | ********* 6 | 7 | 2.1.0 (2024-12-29) 8 | ################### 9 | 10 | * Added support for ``prompt_toolkit`` versions 3.0.37 and above. 11 | * Added search functionality to ``select`` and ``checkbox`` prompts, allowing users to search for a prefix, with the list of options filtered accordingly. 12 | * Added ``description`` option to ``checkbox`` and ``select``. 13 | * Added explicit support for Python 3.12. 14 | * Ignore ``Separator`` when calculating the length of choices in ``select``, allowing full utilisation of the maximum number of keyboard shortcuts. 15 | * Fixed issue where setting ``Choice.shortcut_key`` or ``Choice.auto_shortcut`` did not update the other. 16 | * Fixed a bug where disabled ``Choice`` items could still be interacted with via keyboard shortcuts. 17 | * Moved newline in :kbd:`Ctrl+C` message to a constant, allowing users to customise or remove it if desired. 18 | * Updated ``autoflake`` pre-commit hook to the new official repository. 19 | * Removed the ``setup.cfg`` file as it is no longer needed with the current project configuration. 20 | * Fixed minor typos in autocomplete examples. 21 | * Updated dependencies. 22 | 23 | 2.0.1 (2023-09-08) 24 | ################### 25 | 26 | * Updated dependencies. 27 | * Fixed broken documentation build. 28 | 29 | 2.0.0 (2023-07-25) 30 | ################### 31 | 32 | * Updated dependencies. 33 | * Modified default choice selection based on the ``Choice`` value. Now, it is 34 | not necessary to pass the same instance of the ``Choice`` object: the same 35 | ``value`` may be used. 36 | * Fixed various minor bugs in development scripts and continuous integration. 37 | * Improved continuous integration and testing process. 38 | * Added pull request and issue templates to the GitHub repository. 39 | * Implemented lazy function call for obtaining choices. 40 | * Expanded the test matrix to include additional Python versions. 41 | * Added the ability to specify the start point of a file path. 42 | * Enabled displaying arbitrary paths in file path input. 43 | * Allowed skipping of questions in the ``unsafe_ask`` function. 44 | * Resolved typing bugs. 45 | * Included a password confirmation example. 46 | * Now returning selected choices even if they are disabled. 47 | * Added support for Emacs keys (:kbd:`Ctrl+N` and :kbd:`Ctrl+P`). 48 | * Fixed rendering errors in the documentation. 49 | * Introduced a new ``print`` question type. 50 | * Deprecated support for Python 3.6 and 3.7. 51 | * Added dynamic instruction messages for ``checkbox`` and ``confirm``. 52 | * Removed the upper bound from the Python version specifier. 53 | * Added a ``press_any_key_to_continue`` prompt. 54 | 55 | 1.10.0 (2021-07-10) 56 | ################### 57 | 58 | * Use direct image URLs in ``README.md``. 59 | * Switched to ``poetry-core``. 60 | * Relax Python version constraint. 61 | * Add ``pointer`` option to ``checkbox`` and ``select``. 62 | * Change enter instruction for multiline input. 63 | * Removed unnecessary Poetry includes. 64 | * Minor updates to documentation. 65 | * Added additional unit tests. 66 | * Added ``use_arrow_keys`` and ``use_jk_keys`` options to ``checkbox``. 67 | * Added ``use_jk_keys`` and ``show_selected`` options to ``select``. 68 | * Fix highlighting bug when using ``default`` parameter for ``select``. 69 | 70 | 1.9.0 (2020-12-20) 71 | ################## 72 | 73 | * Added brand new documentation https://questionary.readthedocs.io/ 74 | (thanks to `@kiancross `_) 75 | 76 | 1.8.1 (2020-11-17) 77 | ################## 78 | 79 | * Fixed regression for checkboxes where all values are returned as strings 80 | fixes `#88 `_. 81 | 82 | 1.8.0 (2020-11-08) 83 | ################## 84 | 85 | * Added additional question type ``questionary.path`` 86 | * Added possibility to validate select and checkboxes selections before 87 | submitting them. 88 | * Added a helper to print formatted text ``questionary.print``. 89 | * Added API method to call prompt in an unsafe way. 90 | * Hide cursor on select only showing the item marker. 91 | 92 | 1.7.0 (2002-10-15) 93 | ################## 94 | 95 | * Added support for Python 3.9. 96 | * Better UX for multiline text input. 97 | * Allow passing custom lexer. 98 | 99 | 1.6.0 (2020-10-04) 100 | ################## 101 | 102 | * Updated black code style formatting and fixed version. 103 | * Fixed colour of answer for some prompts. 104 | * Added ``py.typed`` marker file. 105 | * Documented multiline input for devs and users and added tests. 106 | * Accept style tuples in ``title`` argument annotation of ``Choice``. 107 | * Added ``default`` for select and ``initial_choice`` for checkbox 108 | prompts. 109 | * Removed check for choices if completer is present. 110 | 111 | 1.5.2 (2020-04-16) 112 | ################## 113 | 114 | Bug fix release. 115 | 116 | * Added ``.ask_async`` support for forms. 117 | 118 | 1.5.1 (2020-01-22) 119 | ################## 120 | 121 | Bug fix release. 122 | 123 | * Fixed ``.ask_async`` for questions on ``prompt_toolkit==2.*``. 124 | Added tests for it. 125 | 126 | 1.5.0 (2020-01-22) 127 | ################## 128 | 129 | Feature release. 130 | 131 | * Added support for ``prompt_toolkit`` 3. 132 | * All tests will be run against ``prompt_toolkit`` 2 and 3. 133 | * Removed support for Python 3.5 (``prompt_toolkit`` 3 does not support 134 | that any more). 135 | 136 | 1.4.0 (2019-11-10) 137 | ################## 138 | 139 | Feature release. 140 | 141 | * Added additional question type ``autocomplete``. 142 | * Allow pointer and highlight in select question type. 143 | 144 | 1.3.0 (2019-08-25) 145 | ################## 146 | 147 | Feature release. 148 | 149 | * Add additional options to style checkboxes and select prompts 150 | `#14 `_. 151 | 152 | 1.2.1 (2019-08-19) 153 | ################## 154 | 155 | Bug fix release. 156 | 157 | * Fixed compatibility with Python 3.5.2 by removing ``Type`` annotation 158 | (this time for real). 159 | 160 | 1.2.0 (2019-07-30) 161 | ################## 162 | 163 | Feature release. 164 | 165 | * Allow a user to pass in a validator as an instance 166 | `#10 `_. 167 | 168 | 1.1.1 (2019-04-21) 169 | ################## 170 | 171 | Bug fix release. 172 | 173 | * Fixed compatibility with python 3.5.2 by removing ``Type`` annotation. 174 | 175 | 1.1.0 (2019-03-10) 176 | ################## 177 | 178 | Feature release. 179 | 180 | * Added ``skip_if`` to questions to allow skipping questions using a flag. 181 | 182 | 1.0.2 (2019-01-23) 183 | ################## 184 | 185 | Bug fix release. 186 | 187 | * Fixed odd behaviour if select is created without providing any choices 188 | instead, we will raise a ``ValueError`` now 189 | `#6 `_. 190 | 191 | 1.0.1 (2019-01-12) 192 | ################## 193 | 194 | Bug fix release, adding some convenience shortcuts. 195 | 196 | * Added shortcut keys :kbd:`j` (move down the list) and :kbd:`k` (move up) to 197 | the prompts ``select`` and ``checkbox`` (fixes 198 | `#2 `_). 199 | 200 | * Fixed unclosed file handle in ``setup.py``. 201 | * Fixed unnecessary empty lines moving selections to far down 202 | (fixes `#3 `_). 203 | 204 | 1.0.0 (2018-12-14) 205 | ################## 206 | 207 | Initial public release of the library. 208 | 209 | * Added python interface. 210 | * Added dict style question creation. 211 | * Improved the documentation. 212 | * More tests and automatic Travis test execution. 213 | -------------------------------------------------------------------------------- /docs/pages/contributors.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | Contributor's Guide 3 | ******************* 4 | 5 | Steps for Submitting Code 6 | ######################### 7 | Contributions are very much welcomed and appreciated. Every little bit of help 8 | counts, so do not hesitate! 9 | 10 | 1. Check for open issues, or open a new issue to start some discussion around 11 | a feature idea or bug. There is a `contributor friendly tag`_ for issues 12 | that should be ideal for people who are not familiar with the codebase yet. 13 | 14 | 2. Fork `the repository `_ on GitHub to 15 | start making your changes. 16 | 17 | 3. `Install Poetry `_. 18 | 19 | 4. Configure development environment. 20 | 21 | .. code-block:: console 22 | 23 | make develop 24 | 25 | 5. Write some tests that show the bug is fixed or that the feature works as 26 | expected. 27 | 28 | 6. Ensure your code passes the code quality checks by running 29 | 30 | .. code-block:: console 31 | 32 | $ make lint 33 | 34 | 7. Check all of the unit tests pass by running 35 | 36 | .. code-block:: console 37 | 38 | $ make test 39 | 40 | 8. Check the type checks pass by running 41 | 42 | .. code-block:: console 43 | 44 | $ make types 45 | 46 | 9. Send a pull request and bug the maintainer until it gets merged and 47 | published 🙂 48 | 49 | .. _`contributor friendly tag`: https://github.com/tmbo/questionary/issues?direction=desc&labels=good+first+issue&page=1&sort=upd 50 | 51 | Bug Reports 52 | ########### 53 | 54 | Bug reports should be made to the 55 | `issue tracker `_. 56 | Please include enough information to reproduce the issue you are having. 57 | A `minimal, reproducible example `_ 58 | would be very helpful. 59 | 60 | Feature Requests 61 | ################ 62 | 63 | Feature requests should be made to the 64 | `issue tracker `_. 65 | 66 | Other 67 | ##### 68 | 69 | Create a New Release 70 | ******************** 71 | 72 | 1. Update the version number in ``questionary/version.py`` and 73 | ``pyproject.toml``. 74 | 75 | 2. Add a new section for the release to :ref:`changelog`. 76 | 3. Commit these changes. 77 | 4. ``git tag`` the commit with the release version number. 78 | 79 | GitHub Actions will build and push the updated library to PyPi. 80 | 81 | Create a Command Line Recording 82 | ******************************* 83 | 84 | 1. Install the following tools: 85 | 86 | .. code-block:: console 87 | 88 | $ brew install asciinema 89 | $ npm install --global asciicast2gif 90 | 91 | 2. Start the recording with ``asciinema``: 92 | 93 | .. code-block:: console 94 | 95 | $ asciinema rec 96 | 97 | 3. Do the thing you want to record. 98 | 99 | 4. Convert to gif using ``asciicast2gif``: 100 | 101 | .. code-block:: console 102 | 103 | $ asciicast2gif -h 7 -w 120 -s 2 output.gif 104 | -------------------------------------------------------------------------------- /docs/pages/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | ************ 4 | Installation 5 | ************ 6 | 7 | Use a Published Release 8 | ####################### 9 | 10 | To install Questionary, simply run this command in your terminal of 11 | choice: 12 | 13 | .. code-block:: console 14 | 15 | $ pip install questionary 16 | 17 | Build from Source 18 | ################# 19 | 20 | Installing from source code is only necessary, if you want to 21 | make changes to the Questionary source code. Questionary is actively 22 | `developed on GitHub `_. 23 | 24 | You can either clone the public repository: 25 | 26 | .. code-block:: console 27 | 28 | $ git clone git@github.com:tmbo/questionary.git 29 | 30 | Or, download the tarball: 31 | 32 | .. code-block:: console 33 | 34 | $ curl -OL https://github.com/tmbo/questionary/tarball/master 35 | 36 | .. note:: 37 | If you are using windows, you can also download a zip instead: 38 | 39 | .. code-block:: console 40 | 41 | $ curl -OL https://github.com/tmbo/questionary/zipball/master 42 | 43 | Questionary uses `Poetry `_ for packaging and 44 | dependency management. If you want to build Questionary from source, you 45 | must install Poetry first: 46 | 47 | .. code-block:: console 48 | 49 | $ curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 50 | 51 | There are several other ways to install Poetry, as seen in 52 | `the official guide `_. 53 | 54 | To install Questionary and its dependencies in editable mode, execute 55 | 56 | .. code-block:: console 57 | 58 | $ make install 59 | -------------------------------------------------------------------------------- /docs/pages/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | ********** 4 | Quickstart 5 | ********** 6 | 7 | Questionary supports two different concepts: 8 | 9 | - creating a **single question** for the user 10 | 11 | .. code-block:: python3 12 | 13 | questionary.password("What's your secret?").ask() 14 | 15 | - creating a **form with multiple questions** asked one after another 16 | 17 | .. code-block:: python3 18 | 19 | answers = questionary.form( 20 | first = questionary.confirm("Would you like the next question?", default=True), 21 | second = questionary.select("Select item", choices=["item1", "item2", "item3"]) 22 | ).ask() 23 | 24 | Asking a Single Question 25 | ======================== 26 | 27 | Questionary ships with a lot of different :ref:`question_types` to provide 28 | the right prompt for the right question. All of them work in the same way though. 29 | Firstly, you create a question: 30 | 31 | .. code:: python3 32 | 33 | import questionary 34 | 35 | question = questionary.text("What's your first name") 36 | 37 | and secondly, you need to prompt the user to answer it: 38 | 39 | .. code:: python3 40 | 41 | answer = question.ask() 42 | 43 | Since our question is a ``text`` prompt, ``answer`` will 44 | contain the text the user typed after they submitted it. 45 | 46 | You can concatenate creating and asking the question in a single 47 | line if you like, e.g. 48 | 49 | .. code:: python3 50 | 51 | import questionary 52 | 53 | answer = questionary.text("What's your first name").ask() 54 | 55 | .. note:: 56 | 57 | There are a lot more question types apart from ``text``. 58 | For a description of the different question types, head 59 | over to the :ref:`question_types`. 60 | 61 | Asking Multiple Questions 62 | ========================= 63 | 64 | You can use the :meth:`~questionary.form` function to ask a collection 65 | of :class:`Questions `. The questions will be asked in 66 | the order they are passed to `:meth:`~questionary.form``. 67 | 68 | .. code:: python3 69 | 70 | import questionary 71 | 72 | answers = questionary.form( 73 | first = questionary.confirm("Would you like the next question?", default=True), 74 | second = questionary.select("Select item", choices=["item1", "item2", "item3"]) 75 | ).ask() 76 | 77 | print(answers) 78 | 79 | The printed output will have the following format: 80 | 81 | .. code-block:: python3 82 | 83 | {'first': True, 'second': 'item2'} 84 | 85 | The :meth:`~questionary.prompt` function also allows you to ask a 86 | collection of questions, however instead of taking :class:`~questionary.Question` 87 | instances, it takes a dictionary: 88 | 89 | .. code:: python3 90 | 91 | import questionary 92 | 93 | questions = [ 94 | { 95 | "type": "confirm", 96 | "name": "first", 97 | "message": "Would you like the next question?", 98 | "default": True, 99 | }, 100 | { 101 | "type": "select", 102 | "name": "second", 103 | "message": "Select item", 104 | "choices": ["item1", "item2", "item3"], 105 | }, 106 | ] 107 | 108 | questionary.prompt(questions) 109 | 110 | The format of the returned answers is the same as the one for 111 | :meth:`~questionary.form`. You can find more details on the configuration 112 | dictionaries in :ref:`question_dictionaries`. 113 | -------------------------------------------------------------------------------- /docs/pages/support.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Support 3 | ******* 4 | 5 | Please `open an issue `_ 6 | with enough information for us to reproduce your problem. 7 | A `minimal, reproducible example `_ 8 | would be very helpful. 9 | -------------------------------------------------------------------------------- /docs/pages/types.rst: -------------------------------------------------------------------------------- 1 | .. _question_types: 2 | 3 | ************** 4 | Question Types 5 | ************** 6 | 7 | The different question types are meant to cover different use cases. The 8 | parameters and configuration options are explained in detail for each 9 | type. But before we get into to many details, here is a **cheatsheet 10 | with the available question types**: 11 | 12 | * use :ref:`type_text` to ask for **free text** input 13 | 14 | * use :ref:`type_password` to ask for free text where the **text is hidden** 15 | 16 | * use :ref:`type_path` to ask for a **file or directory** path with autocompletion 17 | 18 | * use :ref:`type_confirm` to ask a **yes or no** question 19 | 20 | * use :ref:`type_select` to ask the user to select **one item** from a beautiful list 21 | 22 | * use :ref:`type_raw_select` to ask the user to select **one item** from a list 23 | 24 | * use :ref:`type_checkbox` to ask the user to select **any number of items** from a list 25 | 26 | * use :ref:`type_autocomplete` to ask for free text with **autocomplete help** 27 | 28 | * use :ref:`type_press_any_key_to_continue` to ask the user to **press any key to continue** 29 | 30 | .. _type_text: 31 | 32 | Text 33 | #### 34 | 35 | .. automethod:: questionary::text 36 | 37 | .. _type_password: 38 | 39 | Password 40 | ######## 41 | 42 | .. automethod:: questionary::password 43 | 44 | .. _type_path: 45 | 46 | File Path 47 | ######### 48 | 49 | .. automethod:: questionary::path 50 | 51 | .. _type_confirm: 52 | 53 | Confirmation 54 | ############ 55 | 56 | .. automethod:: questionary::confirm 57 | 58 | .. _type_select: 59 | 60 | Select 61 | ###### 62 | 63 | .. automethod:: questionary::select 64 | 65 | .. _type_raw_select: 66 | 67 | Raw Select 68 | ########## 69 | 70 | .. automethod:: questionary::rawselect 71 | 72 | .. _type_checkbox: 73 | 74 | Checkbox 75 | ######## 76 | 77 | .. automethod:: questionary::checkbox 78 | 79 | .. _type_autocomplete: 80 | 81 | Autocomplete 82 | ############ 83 | 84 | .. automethod:: questionary::autocomplete 85 | 86 | Printing Formatted Text 87 | ####################### 88 | 89 | .. automethod:: questionary::print 90 | 91 | .. _type_press_any_key_to_continue: 92 | 93 | Press Any Key To Continue 94 | ######################### 95 | 96 | .. automethod:: questionary::press_any_key_to_continue 97 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | from questionary import Style 2 | 3 | custom_style_fancy = Style( 4 | [ 5 | ("separator", "fg:#cc5454"), 6 | ("qmark", "fg:#673ab7 bold"), 7 | ("question", ""), 8 | ("selected", "fg:#cc5454"), 9 | ("pointer", "fg:#673ab7 bold"), 10 | ("highlighted", "fg:#673ab7 bold"), 11 | ("answer", "fg:#f44336 bold"), 12 | ("text", "fg:#FBE9E7"), 13 | ("disabled", "fg:#858585 italic"), 14 | ] 15 | ) 16 | 17 | custom_style_dope = Style( 18 | [ 19 | ("separator", "fg:#6C6C6C"), 20 | ("qmark", "fg:#FF9D00 bold"), 21 | ("question", ""), 22 | ("selected", "fg:#5F819D"), 23 | ("pointer", "fg:#FF9D00 bold"), 24 | ("answer", "fg:#5F819D bold"), 25 | ] 26 | ) 27 | 28 | custom_style_genius = Style( 29 | [ 30 | ("qmark", "fg:#E91E63 bold"), 31 | ("question", ""), 32 | ("selected", "fg:#673AB7 bold"), 33 | ("answer", "fg:#2196f3 bold"), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /examples/advanced_workflow.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | 3 | from questionary import Separator 4 | from questionary import prompt 5 | 6 | 7 | def ask_dictstyle(**kwargs): 8 | questions = [ 9 | { 10 | # just print a message, don't ask a question 11 | # does not require a name (but if provided, is ignored) and does not return a value 12 | "type": "print", 13 | "name": "intro", 14 | "message": "This example demonstrates advanced features! 🦄", 15 | "style": "bold italic", 16 | }, 17 | { 18 | "type": "confirm", 19 | "name": "conditional_step", 20 | "message": "Would you like the next question?", 21 | "default": True, 22 | }, 23 | { 24 | "type": "text", 25 | "name": "next_question", 26 | "message": "Name this library?", 27 | # Validate if the first question was answered with yes or no 28 | "when": lambda x: x["conditional_step"], 29 | # Only accept questionary as answer 30 | "validate": lambda val: val == "questionary", 31 | }, 32 | { 33 | "type": "select", 34 | "name": "second_question", 35 | "message": "Select item", 36 | "choices": ["item1", "item2", Separator(), "other"], 37 | }, 38 | { 39 | # just print a message, don't ask a question 40 | # does not require a name and does not return a value 41 | "type": "print", 42 | "message": "Please enter a value for 'other'", 43 | "style": "bold italic fg:darkred", 44 | "when": lambda x: x["second_question"] == "other", 45 | }, 46 | { 47 | "type": "text", 48 | # intentionally overwrites result from previous question 49 | "name": "second_question", 50 | "message": "Insert free text", 51 | "when": lambda x: x["second_question"] == "other", 52 | }, 53 | ] 54 | return prompt(questions, **kwargs) 55 | 56 | 57 | if __name__ == "__main__": 58 | pprint(ask_dictstyle()) 59 | -------------------------------------------------------------------------------- /examples/autocomplete_ants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Example for a autocomplete question type. 3 | 4 | Run example by typing `python -m examples.autocomplete` in your console.""" 5 | from pprint import pprint 6 | 7 | import questionary 8 | from examples import custom_style_fancy 9 | from questionary import ValidationError 10 | from questionary import Validator 11 | from questionary import prompt 12 | 13 | 14 | class PolyergusValidator(Validator): 15 | def validate(self, document): 16 | ok = "Polyergus" in document.text 17 | if not ok: 18 | raise ValidationError( 19 | message="Please choose a Polyergus Ant", 20 | cursor_position=len(document.text), 21 | ) # Move cursor to end 22 | 23 | 24 | meta_information = { 25 | "Camponotus pennsylvanicus": "This is an important, destructive pest that" 26 | " attacks fences, poles and buildings", 27 | "Linepithema humile": "It is an invasive species that has been established" 28 | " in many Mediterranean climate areas", 29 | "Eciton burchellii": "Known as army ants, moves almost incessantly" 30 | " over the time it exists", 31 | "Atta colombica": "They are known for cutting grasses and leaves, carrying" 32 | " them to their colonies' nests, and growing fungi on" 33 | " them which they later feed on", 34 | "Polyergus lucidus": "It is an obligatory social parasite, unable to feed" 35 | " itself or look after its brood and reliant on ants" 36 | " of another species of the genus Formica to undertake" 37 | " these tasks.", 38 | "Polyergus rufescens": "Is another specie of slave-making ant.", 39 | } 40 | 41 | 42 | def ask_pystyle(**kwargs): 43 | # create the question object 44 | question = questionary.autocomplete( 45 | "Choose ant species", 46 | validate=PolyergusValidator, 47 | meta_information=meta_information, 48 | choices=[ 49 | "Camponotus pennsylvanicus", 50 | "Linepithema humile", 51 | "Eciton burchellii", 52 | "Atta colombica", 53 | "Polyergus lucidus", 54 | "Polyergus rufescens", 55 | ], 56 | ignore_case=False, 57 | style=custom_style_fancy, 58 | **kwargs, 59 | ) 60 | 61 | # prompt the user for an answer 62 | return question.ask() 63 | 64 | 65 | def ask_dictstyle(**kwargs): 66 | questions = [ 67 | { 68 | "type": "autocomplete", 69 | "name": "ants", 70 | "choices": [ 71 | "Camponotus pennsylvanicus", 72 | "Linepithema humile", 73 | "Eciton burchellii", 74 | "Atta colombica", 75 | "Polyergus lucidus", 76 | "Polyergus rufescens", 77 | ], 78 | "meta_information": meta_information, 79 | "message": "Choose ant species", 80 | "validate": PolyergusValidator, 81 | } 82 | ] 83 | 84 | return prompt(questions, style=custom_style_fancy, **kwargs) 85 | 86 | 87 | if __name__ == "__main__": 88 | pprint(ask_pystyle()) 89 | -------------------------------------------------------------------------------- /examples/checkbox_search.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | from examples import custom_style_dope 3 | 4 | zoo_animals = [ 5 | "Lion", 6 | "Tiger", 7 | "Elephant", 8 | "Giraffe", 9 | "Zebra", 10 | "Panda", 11 | "Kangaroo", 12 | "Gorilla", 13 | "Chimpanzee", 14 | "Orangutan", 15 | "Hippopotamus", 16 | "Rhinoceros", 17 | "Leopard", 18 | "Cheetah", 19 | "Polar Bear", 20 | "Grizzly Bear", 21 | "Penguin", 22 | "Flamingo", 23 | "Peacock", 24 | "Ostrich", 25 | "Emu", 26 | "Koala", 27 | "Sloth", 28 | "Armadillo", 29 | "Meerkat", 30 | "Lemur", 31 | "Red Panda", 32 | "Wolf", 33 | "Fox", 34 | "Otter", 35 | "Sea Lion", 36 | "Walrus", 37 | "Seal", 38 | "Crocodile", 39 | "Alligator", 40 | "Python", 41 | "Boa Constrictor", 42 | "Iguana", 43 | "Komodo Dragon", 44 | "Tortoise", 45 | "Turtle", 46 | "Parrot", 47 | "Toucan", 48 | "Macaw", 49 | "Hyena", 50 | "Jaguar", 51 | "Anteater", 52 | "Capybara", 53 | "Bison", 54 | "Moose", 55 | ] 56 | 57 | 58 | if __name__ == "__main__": 59 | toppings = ( 60 | questionary.checkbox( 61 | "Select animals for your zoo", 62 | choices=zoo_animals, 63 | validate=lambda a: ( 64 | True if len(a) > 0 else "You must select at least one zoo animal" 65 | ), 66 | style=custom_style_dope, 67 | use_jk_keys=False, 68 | use_search_filter=True, 69 | ).ask() 70 | or [] 71 | ) 72 | 73 | print( 74 | f"Alright let's create our zoo with following animals: {', '.join(toppings)}." 75 | ) 76 | -------------------------------------------------------------------------------- /examples/checkbox_separators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Example for a checkbox question type. 3 | 4 | Run example by typing `python -m examples.checkbox` in your console.""" 5 | from pprint import pprint 6 | 7 | import questionary 8 | from examples import custom_style_dope 9 | from questionary import Choice 10 | from questionary import Separator 11 | from questionary import prompt 12 | 13 | 14 | def ask_pystyle(**kwargs): 15 | # create the question object 16 | question = questionary.checkbox( 17 | "Select toppings", 18 | qmark="😃", 19 | choices=[ 20 | Choice("foo", checked=True), 21 | Separator(), 22 | Choice("bar", disabled="nope"), 23 | "bazz", 24 | Separator("--END--"), 25 | ], 26 | style=custom_style_dope, 27 | **kwargs, 28 | ) 29 | 30 | # prompt the user for an answer 31 | return question.ask() 32 | 33 | 34 | def ask_dictstyle(**kwargs): 35 | questions = [ 36 | { 37 | "type": "checkbox", 38 | "qmark": "😃", 39 | "message": "Select toppings", 40 | "name": "toppings", 41 | "choices": [ 42 | {"name": "foo", "checked": True}, 43 | Separator(), 44 | {"name": "bar", "disabled": "nope"}, 45 | "bazz", 46 | Separator("--END--"), 47 | ], 48 | } 49 | ] 50 | 51 | return prompt(questions, style=custom_style_dope, **kwargs) 52 | 53 | 54 | if __name__ == "__main__": 55 | pprint(ask_pystyle()) 56 | -------------------------------------------------------------------------------- /examples/checkbox_toppings.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | from examples import custom_style_dope 3 | 4 | if __name__ == "__main__": 5 | toppings = ( 6 | questionary.checkbox( 7 | "Select toppings", 8 | choices=["foo", "bar", "bazz"], 9 | validate=lambda a: ( 10 | True if len(a) > 0 else "You must select at least one topping" 11 | ), 12 | style=custom_style_dope, 13 | ).ask() 14 | or [] 15 | ) 16 | 17 | print(f"Alright let's go mixing some {' and '.join(toppings)} 🤷‍♂️.") 18 | -------------------------------------------------------------------------------- /examples/confirm_amazed.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | 3 | if __name__ == "__main__": 4 | confirmation = questionary.confirm("Are you amazed?").ask() 5 | 6 | if confirmation: 7 | print("That is amazing! 💥🚀") 8 | else: 9 | print("That is unfortunate 🐡.") 10 | -------------------------------------------------------------------------------- /examples/confirm_continue.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Example for a confirm question type. 3 | 4 | Run example by typing `python -m examples.confirm` in your console.""" 5 | from pprint import pprint 6 | 7 | import questionary 8 | from examples import custom_style_dope 9 | from questionary import prompt 10 | 11 | 12 | def ask_pystyle(**kwargs): 13 | # create the question object 14 | question = questionary.confirm( 15 | "Do you want to continue?", default=True, style=custom_style_dope, **kwargs 16 | ) 17 | 18 | # prompt the user for an answer 19 | return question.ask() 20 | 21 | 22 | def ask_dictstyle(**kwargs): 23 | questions = [ 24 | { 25 | "type": "confirm", 26 | "message": "Do you want to continue?", 27 | "name": "continue", 28 | "default": True, 29 | } 30 | ] 31 | 32 | return prompt(questions, style=custom_style_dope, **kwargs) 33 | 34 | 35 | if __name__ == "__main__": 36 | pprint(ask_pystyle()) 37 | -------------------------------------------------------------------------------- /examples/password_confirm.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | 3 | 4 | def create_password(**kwargs): 5 | x = questionary.password("Password", **kwargs).ask() 6 | y = questionary.password("Repeat password", **kwargs).ask() 7 | 8 | if x == y: 9 | questionary.print("✅ ") 10 | return x 11 | 12 | else: 13 | questionary.print( 14 | "Passwords do not match. Try again.", style="italic fg:darkred" 15 | ) 16 | # until passwords match, we keep repeating the question 17 | return create_password(**kwargs) 18 | 19 | 20 | if __name__ == "__main__": 21 | create_password() 22 | -------------------------------------------------------------------------------- /examples/password_git.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Example for a password question type. 3 | 4 | Run example by typing `python -m examples.password` in your console.""" 5 | from pprint import pprint 6 | 7 | import questionary 8 | from examples import custom_style_dope 9 | from questionary import prompt 10 | 11 | 12 | def ask_pystyle(**kwargs): 13 | # create the question object 14 | question = questionary.password( 15 | "Enter your git password", style=custom_style_dope, **kwargs 16 | ) 17 | 18 | # prompt the user for an answer 19 | return question.ask() 20 | 21 | 22 | def ask_dictstyle(**kwargs): 23 | questions = [ 24 | {"type": "password", "message": "Enter your git password", "name": "password"} 25 | ] 26 | 27 | return prompt(questions, style=custom_style_dope, **kwargs) 28 | 29 | 30 | if __name__ == "__main__": 31 | pprint(ask_pystyle()) 32 | -------------------------------------------------------------------------------- /examples/password_secret.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | 3 | if __name__ == "__main__": 4 | password = questionary.password("What's your secret?").ask() or "" 5 | 6 | print( 7 | f"Your secret is {password[:1]}... no just kidding - " 8 | f"I'm not going to tell anyone. 🤫" 9 | ) 10 | -------------------------------------------------------------------------------- /examples/project_path.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | 3 | if __name__ == "__main__": 4 | path = questionary.path("Path to the projects version file").ask() 5 | if path: 6 | print(f"Found version file at {path} 🦄") 7 | else: 8 | print("No version file it is then!") 9 | -------------------------------------------------------------------------------- /examples/rawselect_action.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | 3 | if __name__ == "__main__": 4 | action = ( 5 | questionary.rawselect( 6 | "What do you want to do?", 7 | choices=["Order a pizza", "Make a reservation", "Ask for opening hours"], 8 | ).ask() 9 | or "do nothing" 10 | ) 11 | 12 | print(f"Sorry, I can't {action}. Bye! 🙅") 13 | -------------------------------------------------------------------------------- /examples/rawselect_separator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Example for rawselect question type. 3 | 4 | Run example by typing `python -m examples.rawselect` in your console.""" 5 | from pprint import pprint 6 | 7 | import questionary 8 | from examples import custom_style_dope 9 | from questionary import Separator 10 | from questionary import prompt 11 | 12 | 13 | def ask_pystyle(**kwargs): 14 | # create the question object 15 | question = questionary.rawselect( 16 | "What do you want to do?", 17 | choices=[ 18 | "Order a pizza", 19 | "Make a reservation", 20 | Separator(), 21 | "Ask opening hours", 22 | "Talk to the receptionist", 23 | ], 24 | style=custom_style_dope, 25 | **kwargs, 26 | ) 27 | 28 | # prompt the user for an answer 29 | return question.ask() 30 | 31 | 32 | def ask_dictstyle(**kwargs): 33 | questions = [ 34 | { 35 | "type": "rawselect", 36 | "name": "theme", 37 | "message": "What do you want to do?", 38 | "choices": [ 39 | "Order a pizza", 40 | "Make a reservation", 41 | Separator(), 42 | "Ask opening hours", 43 | "Talk to the receptionist", 44 | ], 45 | }, 46 | ] 47 | 48 | return prompt(questions, style=custom_style_dope, **kwargs) 49 | 50 | 51 | if __name__ == "__main__": 52 | pprint(ask_pystyle()) 53 | -------------------------------------------------------------------------------- /examples/readme.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | from examples import custom_style_dope 3 | 4 | if __name__ == "__main__": 5 | questionary.text("What's your first name").ask() 6 | questionary.password("What's your secret?").ask() 7 | questionary.confirm("Are you amazed?").ask() 8 | questionary.select( 9 | "What do you want to do?", 10 | choices=["Order a pizza", "Make a reservation", "Ask for opening hours"], 11 | ).ask() 12 | questionary.rawselect( 13 | "What do you want to do?", 14 | choices=["Order a pizza", "Make a reservation", "Ask for opening hours"], 15 | ).ask() 16 | questionary.checkbox( 17 | "Select toppings", choices=["foo", "bar", "bazz"], style=custom_style_dope 18 | ).ask() 19 | questionary.path("Path to the projects version file").ask() 20 | -------------------------------------------------------------------------------- /examples/select_action.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | 3 | if __name__ == "__main__": 4 | action = ( 5 | questionary.select( 6 | "What do you want to do?", 7 | choices=["Order a pizza", "Make a reservation", "Ask for opening hours"], 8 | ).ask() 9 | or "do nothing" 10 | ) 11 | 12 | print(f"Sorry, I can't {action}. Bye! 👋") 13 | -------------------------------------------------------------------------------- /examples/select_restaurant.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Example for a select question type. 3 | 4 | Run example by typing `python -m examples.select` in your console.""" 5 | from pprint import pprint 6 | 7 | import questionary 8 | from examples import custom_style_dope 9 | from questionary import Choice 10 | from questionary import Separator 11 | from questionary import prompt 12 | 13 | 14 | def ask_pystyle(**kwargs): 15 | # create the question object 16 | question = questionary.select( 17 | "What do you want to do?", 18 | qmark="😃", 19 | choices=[ 20 | "Order a pizza", 21 | "Make a reservation", 22 | Separator(), 23 | "Ask for opening hours", 24 | Choice("Contact support", disabled="Unavailable at this time"), 25 | "Talk to the receptionist", 26 | ], 27 | style=custom_style_dope, 28 | **kwargs, 29 | ) 30 | 31 | # prompt the user for an answer 32 | return question.ask() 33 | 34 | 35 | def ask_dictstyle(**kwargs): 36 | questions = [ 37 | { 38 | "type": "select", 39 | "name": "theme", 40 | "message": "What do you want to do?", 41 | "choices": [ 42 | "Order a pizza", 43 | "Make a reservation", 44 | Separator(), 45 | "Ask for opening hours", 46 | {"name": "Contact support", "disabled": "Unavailable at this time"}, 47 | "Talk to the receptionist", 48 | ], 49 | } 50 | ] 51 | 52 | return prompt(questions, style=custom_style_dope, **kwargs) 53 | 54 | 55 | if __name__ == "__main__": 56 | pprint(ask_pystyle()) 57 | -------------------------------------------------------------------------------- /examples/select_search.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Example for a select question type with search enabled. 3 | 4 | Run example by typing `python -m examples.select_search` in your console.""" 5 | from pprint import pprint 6 | 7 | import questionary 8 | from examples import custom_style_dope 9 | from questionary import Choice 10 | from questionary import Separator 11 | from questionary import prompt 12 | 13 | 14 | def ask_pystyle(**kwargs): 15 | # create the question object 16 | question = questionary.select( 17 | "What do you want to do?", 18 | qmark="😃", 19 | choices=[ 20 | "Order a pizza", 21 | "Make a reservation", 22 | "Cancel a reservation", 23 | "Modify your order", 24 | Separator(), 25 | "Ask for opening hours", 26 | Choice("Contact support", disabled="Unavailable at this time"), 27 | "Talk to the receptionist", 28 | ], 29 | style=custom_style_dope, 30 | use_jk_keys=False, 31 | use_search_filter=True, 32 | **kwargs, 33 | ) 34 | 35 | # prompt the user for an answer 36 | return question.ask() 37 | 38 | 39 | def ask_dictstyle(**kwargs): 40 | questions = [ 41 | { 42 | "type": "select", 43 | "name": "theme", 44 | "message": "What do you want to do?", 45 | "choices": [ 46 | "Order a pizza", 47 | "Make a reservation", 48 | "Cancel a reservation", 49 | "Modify your order", 50 | Separator(), 51 | "Ask for opening hours", 52 | {"name": "Contact support", "disabled": "Unavailable at this time"}, 53 | "Talk to the receptionist", 54 | ], 55 | } 56 | ] 57 | 58 | return prompt(questions, style=custom_style_dope, **kwargs) 59 | 60 | 61 | if __name__ == "__main__": 62 | pprint(ask_pystyle()) 63 | -------------------------------------------------------------------------------- /examples/simple_print.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | 3 | if __name__ == "__main__": 4 | questionary.print("Hello World 🦄", style="bold italic fg:darkred") 5 | -------------------------------------------------------------------------------- /examples/text_name.py: -------------------------------------------------------------------------------- 1 | import questionary 2 | 3 | if __name__ == "__main__": 4 | name = questionary.text("What's your first name?").ask() 5 | print(f"Hey {name} 🦄") 6 | -------------------------------------------------------------------------------- /examples/text_phone_number.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Example for a text question type. 3 | 4 | Run example by typing `python -m examples.text` in your console.""" 5 | import re 6 | from pprint import pprint 7 | 8 | import questionary 9 | from examples import custom_style_dope 10 | from questionary import ValidationError 11 | from questionary import Validator 12 | from questionary import prompt 13 | 14 | 15 | class PhoneNumberValidator(Validator): 16 | def validate(self, document): 17 | ok = re.match( 18 | r"^([01])?[-.\s]?\(?(\d{3})\)?" 19 | r"[-.\s]?(\d{3})[-.\s]?(\d{4})\s?" 20 | r"((?:#|ext\.?\s?|x\.?\s?)(?:\d+)?)?$", 21 | document.text, 22 | ) 23 | if not ok: 24 | raise ValidationError( 25 | message="Please enter a valid phone number", 26 | cursor_position=len(document.text), 27 | ) # Move cursor to end 28 | 29 | 30 | def ask_pystyle(**kwargs): 31 | # create the question object 32 | question = questionary.text( 33 | "What's your phone number", 34 | validate=PhoneNumberValidator, 35 | style=custom_style_dope, 36 | **kwargs, 37 | ) 38 | 39 | # prompt the user for an answer 40 | return question.ask() 41 | 42 | 43 | def ask_dictstyle(**kwargs): 44 | questions = [ 45 | { 46 | "type": "text", 47 | "name": "phone", 48 | "message": "What's your phone number", 49 | "validate": PhoneNumberValidator, 50 | } 51 | ] 52 | 53 | return prompt(questions, style=custom_style_dope, **kwargs) 54 | 55 | 56 | if __name__ == "__main__": 57 | pprint(ask_pystyle()) 58 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.black] 6 | line-length = 88 7 | target-version = ["py38", "py39", "py310", "py311", "py312"] 8 | exclude = "((.eggs | .git | .pytype | .pytest_cache | build | dist))" 9 | 10 | [tool.mypy] 11 | ignore_missing_imports = true 12 | show_error_codes = true 13 | warn_redundant_casts = true 14 | warn_unused_ignores = true 15 | 16 | [tool.poetry] 17 | name = "questionary" 18 | version = "2.1.0" 19 | description = "Python library to build pretty command line user prompts ⭐️" 20 | authors = [ "Tom Bocklisch ",] 21 | maintainers = [ "Tom Bocklisch ", "Kian Cross "] 22 | repository = "https://github.com/tmbo/questionary" 23 | documentation = "https://questionary.readthedocs.io/" 24 | classifiers=[ 25 | "Development Status :: 4 - Beta", 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: MIT License", 28 | # supported python versions 29 | "Programming Language :: Python", 30 | "Programming Language :: Python :: 3.8", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | "Programming Language :: Python :: 3.12", 35 | "Topic :: Software Development :: Libraries", 36 | ] 37 | keywords = [ "cli", "ui", "inquirer", "questions", "prompt",] 38 | readme = "README.md" 39 | license = "MIT" 40 | 41 | [tool.poetry.dependencies] 42 | python = ">=3.9" 43 | prompt_toolkit = ">=2.0,<4.0" 44 | 45 | [tool.poetry.group.docs] 46 | optional = true 47 | 48 | [tool.poetry.group.docs.dependencies] 49 | Sphinx = ">=4.1,<8.0" 50 | sphinx-rtd-theme = ">=0.5,<3.1" 51 | sphinx-autobuild = ">=2020.9.1,<2022.0.0" 52 | sphinx-copybutton = ">=0.3.1,<0.6.0" 53 | sphinx-autodoc-typehints = ">=1.11.1,<3.0.0" 54 | 55 | [tool.poetry.group.dev.dependencies] 56 | pytest-cov = ">=5.0.0,<6" 57 | pytest = ">=7.0.1,<9.0.0" 58 | coveralls = "^3.3.1" 59 | mypy = "^1.2.0" 60 | toml = "^0.10.2" 61 | pre-commit = { version = ">=2.20,<4.0", python = ">=3.7,<4.0"} 62 | -------------------------------------------------------------------------------- /questionary/__init__.py: -------------------------------------------------------------------------------- 1 | # noinspection PyUnresolvedReferences 2 | from prompt_toolkit.styles import Style 3 | from prompt_toolkit.validation import ValidationError 4 | from prompt_toolkit.validation import Validator 5 | 6 | import questionary.version 7 | from questionary.form import Form 8 | from questionary.form import FormField 9 | from questionary.form import form 10 | from questionary.prompt import prompt 11 | from questionary.prompt import unsafe_prompt 12 | 13 | # import the shortcuts to create single question prompts 14 | from questionary.prompts.autocomplete import autocomplete 15 | from questionary.prompts.checkbox import checkbox 16 | from questionary.prompts.common import Choice 17 | from questionary.prompts.common import Separator 18 | from questionary.prompts.common import print_formatted_text as print 19 | from questionary.prompts.confirm import confirm 20 | from questionary.prompts.password import password 21 | from questionary.prompts.path import path 22 | from questionary.prompts.press_any_key_to_continue import press_any_key_to_continue 23 | from questionary.prompts.rawselect import rawselect 24 | from questionary.prompts.select import select 25 | from questionary.prompts.text import text 26 | from questionary.question import Question 27 | 28 | __version__ = questionary.version.__version__ 29 | 30 | __all__ = [ 31 | "__version__", 32 | # question types 33 | "autocomplete", 34 | "checkbox", 35 | "confirm", 36 | "password", 37 | "path", 38 | "press_any_key_to_continue", 39 | "rawselect", 40 | "select", 41 | "text", 42 | # utility methods 43 | "print", 44 | "form", 45 | "prompt", 46 | "unsafe_prompt", 47 | # commonly used classes 48 | "Form", 49 | "FormField", 50 | "Question", 51 | "Choice", 52 | "Style", 53 | "Separator", 54 | "Validator", 55 | "ValidationError", 56 | ] 57 | -------------------------------------------------------------------------------- /questionary/constants.py: -------------------------------------------------------------------------------- 1 | from questionary import Style 2 | 3 | # Value to display as an answer when "affirming" a confirmation question 4 | YES = "Yes" 5 | 6 | # Value to display as an answer when "denying" a confirmation question 7 | NO = "No" 8 | 9 | # Instruction text for a confirmation question (yes is default) 10 | YES_OR_NO = "(Y/n)" 11 | 12 | # Instruction text for a confirmation question (no is default) 13 | NO_OR_YES = "(y/N)" 14 | 15 | # Instruction for multiline input 16 | INSTRUCTION_MULTILINE = "(Finish with 'Alt+Enter' or 'Esc then Enter')\n>" 17 | 18 | # Selection token used to indicate the selection cursor in a list 19 | DEFAULT_SELECTED_POINTER = "»" 20 | 21 | # Item prefix to identify selected items in a checkbox list 22 | INDICATOR_SELECTED = "●" 23 | 24 | # Item prefix to identify unselected items in a checkbox list 25 | INDICATOR_UNSELECTED = "○" 26 | 27 | # Prefix displayed in front of questions 28 | DEFAULT_QUESTION_PREFIX = "?" 29 | 30 | # Message shown when a user aborts a question prompt using CTRL-C 31 | DEFAULT_KBI_MESSAGE = "\nCancelled by user\n" 32 | 33 | # Default text shown when the input is invalid 34 | INVALID_INPUT = "Invalid input" 35 | 36 | # Default message style 37 | DEFAULT_STYLE = Style( 38 | [ 39 | ("qmark", "fg:#5f819d"), # token in front of the question 40 | ("question", "bold"), # question text 41 | ("answer", "fg:#FF9D00 bold"), # submitted answer text behind the question 42 | ( 43 | "search_success", 44 | "noinherit fg:#00FF00 bold", 45 | ), # submitted answer text behind the question 46 | ( 47 | "search_none", 48 | "noinherit fg:#FF0000 bold", 49 | ), # submitted answer text behind the question 50 | ("pointer", ""), # pointer used in select and checkbox prompts 51 | ("selected", ""), # style for a selected item of a checkbox 52 | ("separator", ""), # separator in lists 53 | ("instruction", ""), # user instructions for select, rawselect, checkbox 54 | ("text", ""), # any other text 55 | ("instruction", ""), # user instructions for select, rawselect, checkbox 56 | ] 57 | ) 58 | -------------------------------------------------------------------------------- /questionary/form.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from typing import Dict 3 | from typing import NamedTuple 4 | from typing import Sequence 5 | 6 | from questionary.constants import DEFAULT_KBI_MESSAGE 7 | from questionary.question import Question 8 | 9 | 10 | class FormField(NamedTuple): 11 | """ 12 | Represents a question within a form 13 | 14 | Args: 15 | key: The name of the form field. 16 | question: The question to ask in the form field. 17 | """ 18 | 19 | key: str 20 | question: Question 21 | 22 | 23 | def form(**kwargs: Question) -> "Form": 24 | """Create a form with multiple questions. 25 | 26 | The parameter name of a question will be the key for the answer in 27 | the returned dict. 28 | 29 | Args: 30 | kwargs: Questions to ask in the form. 31 | """ 32 | return Form(*(FormField(k, q) for k, q in kwargs.items())) 33 | 34 | 35 | class Form: 36 | """Multi question prompts. Questions are asked one after another. 37 | 38 | All the answers are returned as a dict with one entry per question. 39 | 40 | This class should not be invoked directly, instead use :func:`form`. 41 | """ 42 | 43 | form_fields: Sequence[FormField] 44 | 45 | def __init__(self, *form_fields: FormField) -> None: 46 | self.form_fields = form_fields 47 | 48 | def unsafe_ask(self, patch_stdout: bool = False) -> Dict[str, Any]: 49 | """Ask the questions synchronously and return user response. 50 | 51 | Does not catch keyboard interrupts. 52 | 53 | Args: 54 | patch_stdout: Ensure that the prompt renders correctly if other threads 55 | are printing to stdout. 56 | 57 | Returns: 58 | The answers from the form. 59 | """ 60 | return {f.key: f.question.unsafe_ask(patch_stdout) for f in self.form_fields} 61 | 62 | async def unsafe_ask_async(self, patch_stdout: bool = False) -> Dict[str, Any]: 63 | """Ask the questions using asyncio and return user response. 64 | 65 | Does not catch keyboard interrupts. 66 | 67 | Args: 68 | patch_stdout: Ensure that the prompt renders correctly if other threads 69 | are printing to stdout. 70 | 71 | Returns: 72 | The answers from the form. 73 | """ 74 | return { 75 | f.key: await f.question.unsafe_ask_async(patch_stdout) 76 | for f in self.form_fields 77 | } 78 | 79 | def ask( 80 | self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE 81 | ) -> Dict[str, Any]: 82 | """Ask the questions synchronously and return user response. 83 | 84 | Args: 85 | patch_stdout: Ensure that the prompt renders correctly if other threads 86 | are printing to stdout. 87 | 88 | kbi_msg: The message to be printed on a keyboard interrupt. 89 | 90 | Returns: 91 | The answers from the form. 92 | """ 93 | try: 94 | return self.unsafe_ask(patch_stdout) 95 | except KeyboardInterrupt: 96 | print(kbi_msg) 97 | return {} 98 | 99 | async def ask_async( 100 | self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE 101 | ) -> Dict[str, Any]: 102 | """Ask the questions using asyncio and return user response. 103 | 104 | Args: 105 | patch_stdout: Ensure that the prompt renders correctly if other threads 106 | are printing to stdout. 107 | 108 | kbi_msg: The message to be printed on a keyboard interrupt. 109 | 110 | Returns: 111 | The answers from the form. 112 | """ 113 | try: 114 | return await self.unsafe_ask_async(patch_stdout) 115 | except KeyboardInterrupt: 116 | print(kbi_msg) 117 | return {} 118 | -------------------------------------------------------------------------------- /questionary/prompt.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from typing import Dict 3 | from typing import Iterable 4 | from typing import Mapping 5 | from typing import Optional 6 | from typing import Union 7 | 8 | from prompt_toolkit.output import ColorDepth 9 | 10 | from questionary import utils 11 | from questionary.constants import DEFAULT_KBI_MESSAGE 12 | from questionary.prompts import AVAILABLE_PROMPTS 13 | from questionary.prompts import prompt_by_name 14 | from questionary.prompts.common import print_formatted_text 15 | 16 | 17 | class PromptParameterException(ValueError): 18 | """Received a prompt with a missing parameter.""" 19 | 20 | def __init__(self, message: str, errors: Optional[BaseException] = None) -> None: 21 | # Call the base class constructor with the parameters it needs 22 | super().__init__(f"You must provide a `{message}` value", errors) 23 | 24 | 25 | def prompt( 26 | questions: Union[Dict[str, Any], Iterable[Mapping[str, Any]]], 27 | answers: Optional[Mapping[str, Any]] = None, 28 | patch_stdout: bool = False, 29 | true_color: bool = False, 30 | kbi_msg: str = DEFAULT_KBI_MESSAGE, 31 | **kwargs: Any, 32 | ) -> Dict[str, Any]: 33 | """Prompt the user for input on all the questions. 34 | 35 | Catches keyboard interrupts and prints a message. 36 | 37 | See :func:`unsafe_prompt` for possible question configurations. 38 | 39 | Args: 40 | questions: A list of question configs representing questions to 41 | ask. A question config may have the following options: 42 | 43 | * type - The type of question. 44 | * name - An ID for the question (to identify it in the answers :obj:`dict`). 45 | 46 | * when - Callable to conditionally show the question. This function 47 | takes a :obj:`dict` representing the current answers. 48 | 49 | * filter - Function that the answer is passed to. The return value of this 50 | function is saved as the answer. 51 | 52 | Additional options correspond to the parameter names for 53 | particular question types. 54 | 55 | answers: Default answers. 56 | 57 | patch_stdout: Ensure that the prompt renders correctly if other threads 58 | are printing to stdout. 59 | 60 | kbi_msg: The message to be printed on a keyboard interrupt. 61 | true_color: Use true color output. 62 | 63 | color_depth: Color depth to use. If ``true_color`` is set to true then this 64 | value is ignored. 65 | 66 | type: Default ``type`` value to use in question config. 67 | filter: Default ``filter`` value to use in question config. 68 | name: Default ``name`` value to use in question config. 69 | when: Default ``when`` value to use in question config. 70 | default: Default ``default`` value to use in question config. 71 | kwargs: Additional options passed to every question. 72 | 73 | Returns: 74 | Dictionary of question answers. 75 | """ 76 | 77 | try: 78 | return unsafe_prompt(questions, answers, patch_stdout, true_color, **kwargs) 79 | except KeyboardInterrupt: 80 | print(kbi_msg) 81 | return {} 82 | 83 | 84 | def unsafe_prompt( 85 | questions: Union[Dict[str, Any], Iterable[Mapping[str, Any]]], 86 | answers: Optional[Mapping[str, Any]] = None, 87 | patch_stdout: bool = False, 88 | true_color: bool = False, 89 | **kwargs: Any, 90 | ) -> Dict[str, Any]: 91 | """Prompt the user for input on all the questions. 92 | 93 | Won't catch keyboard interrupts. 94 | 95 | Args: 96 | questions: A list of question configs representing questions to 97 | ask. A question config may have the following options: 98 | 99 | * type - The type of question. 100 | * name - An ID for the question (to identify it in the answers :obj:`dict`). 101 | 102 | * when - Callable to conditionally show the question. This function 103 | takes a :obj:`dict` representing the current answers. 104 | 105 | * filter - Function that the answer is passed to. The return value of this 106 | function is saved as the answer. 107 | 108 | Additional options correspond to the parameter names for 109 | particular question types. 110 | 111 | answers: Default answers. 112 | 113 | patch_stdout: Ensure that the prompt renders correctly if other threads 114 | are printing to stdout. 115 | 116 | true_color: Use true color output. 117 | 118 | color_depth: Color depth to use. If ``true_color`` is set to true then this 119 | value is ignored. 120 | 121 | type: Default ``type`` value to use in question config. 122 | filter: Default ``filter`` value to use in question config. 123 | name: Default ``name`` value to use in question config. 124 | when: Default ``when`` value to use in question config. 125 | default: Default ``default`` value to use in question config. 126 | kwargs: Additional options passed to every question. 127 | 128 | Returns: 129 | Dictionary of question answers. 130 | 131 | Raises: 132 | KeyboardInterrupt: raised on keyboard interrupt 133 | """ 134 | 135 | if isinstance(questions, dict): 136 | questions = [questions] 137 | 138 | answers = dict(answers or {}) 139 | 140 | for question_config in questions: 141 | question_config = dict(question_config) 142 | # import the question 143 | if "type" not in question_config: 144 | raise PromptParameterException("type") 145 | # every type except 'print' needs a name 146 | if "name" not in question_config and question_config["type"] != "print": 147 | raise PromptParameterException("name") 148 | 149 | _kwargs = kwargs.copy() 150 | _kwargs.update(question_config) 151 | 152 | _type = _kwargs.pop("type") 153 | _filter = _kwargs.pop("filter", None) 154 | name = _kwargs.pop("name", None) if _type == "print" else _kwargs.pop("name") 155 | when = _kwargs.pop("when", None) 156 | 157 | if true_color: 158 | _kwargs["color_depth"] = ColorDepth.TRUE_COLOR 159 | 160 | if when: 161 | # at least a little sanity check! 162 | if callable(question_config["when"]): 163 | try: 164 | if not question_config["when"](answers): 165 | continue 166 | except Exception as exception: 167 | raise ValueError( 168 | f"Problem in 'when' check of " f"{name} question: {exception}" 169 | ) from exception 170 | else: 171 | raise ValueError( 172 | "'when' needs to be function that accepts a dict argument" 173 | ) 174 | 175 | # handle 'print' type 176 | if _type == "print": 177 | try: 178 | message = _kwargs.pop("message") 179 | except KeyError as e: 180 | raise PromptParameterException("message") from e 181 | 182 | # questions can take 'input' arg but print_formatted_text does not 183 | # Remove 'input', if present, to avoid breaking during tests 184 | _kwargs.pop("input", None) 185 | 186 | print_formatted_text(message, **_kwargs) 187 | if name: 188 | answers[name] = None 189 | continue 190 | 191 | choices = question_config.get("choices") 192 | if choices is not None and callable(choices): 193 | calculated_choices = choices(answers) 194 | question_config["choices"] = calculated_choices 195 | kwargs["choices"] = calculated_choices 196 | 197 | if _filter: 198 | # at least a little sanity check! 199 | if not callable(_filter): 200 | raise ValueError( 201 | "'filter' needs to be function that accepts an argument" 202 | ) 203 | 204 | if callable(question_config.get("default")): 205 | _kwargs["default"] = question_config["default"](answers) 206 | 207 | create_question_func = prompt_by_name(_type) 208 | 209 | if not create_question_func: 210 | raise ValueError( 211 | f"No question type '{_type}' found. " 212 | f"Known question types are {', '.join(AVAILABLE_PROMPTS)}." 213 | ) 214 | 215 | missing_args = list(utils.missing_arguments(create_question_func, _kwargs)) 216 | if missing_args: 217 | raise PromptParameterException(missing_args[0]) 218 | 219 | question = create_question_func(**_kwargs) 220 | 221 | answer = question.unsafe_ask(patch_stdout) 222 | 223 | if answer is not None: 224 | if _filter: 225 | try: 226 | answer = _filter(answer) 227 | except Exception as exception: 228 | raise ValueError( 229 | f"Problem processing 'filter' of {name} " 230 | f"question: {exception}" 231 | ) from exception 232 | answers[name] = answer 233 | 234 | return answers 235 | -------------------------------------------------------------------------------- /questionary/prompts/__init__.py: -------------------------------------------------------------------------------- 1 | from questionary.prompts import autocomplete 2 | from questionary.prompts import checkbox 3 | from questionary.prompts import confirm 4 | from questionary.prompts import password 5 | from questionary.prompts import path 6 | from questionary.prompts import press_any_key_to_continue 7 | from questionary.prompts import rawselect 8 | from questionary.prompts import select 9 | from questionary.prompts import text 10 | 11 | AVAILABLE_PROMPTS = { 12 | "autocomplete": autocomplete.autocomplete, 13 | "confirm": confirm.confirm, 14 | "text": text.text, 15 | "select": select.select, 16 | "rawselect": rawselect.rawselect, 17 | "password": password.password, 18 | "checkbox": checkbox.checkbox, 19 | "path": path.path, 20 | "press_any_key_to_continue": press_any_key_to_continue.press_any_key_to_continue, 21 | # backwards compatible names 22 | "list": select.select, 23 | "rawlist": rawselect.rawselect, 24 | "input": text.text, 25 | } 26 | 27 | 28 | def prompt_by_name(name): 29 | return AVAILABLE_PROMPTS.get(name) 30 | -------------------------------------------------------------------------------- /questionary/prompts/autocomplete.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from typing import Callable 3 | from typing import Dict 4 | from typing import Iterable 5 | from typing import List 6 | from typing import Optional 7 | from typing import Tuple 8 | from typing import Union 9 | 10 | from prompt_toolkit.completion import CompleteEvent 11 | from prompt_toolkit.completion import Completer 12 | from prompt_toolkit.completion import Completion 13 | from prompt_toolkit.document import Document 14 | from prompt_toolkit.formatted_text import HTML 15 | from prompt_toolkit.lexers import SimpleLexer 16 | from prompt_toolkit.shortcuts.prompt import CompleteStyle 17 | from prompt_toolkit.shortcuts.prompt import PromptSession 18 | from prompt_toolkit.styles import Style 19 | 20 | from questionary.constants import DEFAULT_QUESTION_PREFIX 21 | from questionary.prompts.common import build_validator 22 | from questionary.question import Question 23 | from questionary.styles import merge_styles_default 24 | 25 | 26 | class WordCompleter(Completer): 27 | choices_source: Union[List[str], Callable[[], List[str]]] 28 | ignore_case: bool 29 | meta_information: Dict[str, Any] 30 | match_middle: bool 31 | 32 | def __init__( 33 | self, 34 | choices: Union[List[str], Callable[[], List[str]]], 35 | ignore_case: bool = True, 36 | meta_information: Optional[Dict[str, Any]] = None, 37 | match_middle: bool = True, 38 | ) -> None: 39 | self.choices_source = choices 40 | self.ignore_case = ignore_case 41 | self.meta_information = meta_information or {} 42 | self.match_middle = match_middle 43 | 44 | def _choices(self) -> Iterable[str]: 45 | return ( 46 | self.choices_source() 47 | if callable(self.choices_source) 48 | else self.choices_source 49 | ) 50 | 51 | def _choice_matches(self, word_before_cursor: str, choice: str) -> int: 52 | """Match index if found, -1 if not.""" 53 | 54 | if self.ignore_case: 55 | choice = choice.lower() 56 | 57 | if self.match_middle: 58 | return choice.find(word_before_cursor) 59 | elif choice.startswith(word_before_cursor): 60 | return 0 61 | else: 62 | return -1 63 | 64 | @staticmethod 65 | def _display_for_choice(choice: str, index: int, word_before_cursor: str) -> HTML: 66 | return HTML("{}{}{}").format( 67 | choice[:index], 68 | choice[index : index + len(word_before_cursor)], # noqa: E203 69 | choice[index + len(word_before_cursor) : len(choice)], # noqa: E203 70 | ) 71 | 72 | def get_completions( 73 | self, document: Document, complete_event: CompleteEvent 74 | ) -> Iterable[Completion]: 75 | choices = self._choices() 76 | 77 | # Get word/text before cursor. 78 | word_before_cursor = document.text_before_cursor 79 | 80 | if self.ignore_case: 81 | word_before_cursor = word_before_cursor.lower() 82 | 83 | for choice in choices: 84 | index = self._choice_matches(word_before_cursor, choice) 85 | if index == -1: 86 | # didn't find a match 87 | continue 88 | 89 | display_meta = self.meta_information.get(choice, "") 90 | display = self._display_for_choice(choice, index, word_before_cursor) 91 | 92 | yield Completion( 93 | choice, 94 | start_position=-len(choice), 95 | display=display.formatted_text, 96 | display_meta=display_meta, 97 | style="class:answer", 98 | selected_style="class:selected", 99 | ) 100 | 101 | 102 | def autocomplete( 103 | message: str, 104 | choices: List[str], 105 | default: str = "", 106 | qmark: str = DEFAULT_QUESTION_PREFIX, 107 | completer: Optional[Completer] = None, 108 | meta_information: Optional[Dict[str, Any]] = None, 109 | ignore_case: bool = True, 110 | match_middle: bool = True, 111 | complete_style: CompleteStyle = CompleteStyle.COLUMN, 112 | validate: Any = None, 113 | style: Optional[Style] = None, 114 | **kwargs: Any, 115 | ) -> Question: 116 | """Prompt the user to enter a message with autocomplete help. 117 | 118 | Example: 119 | >>> import questionary 120 | >>> questionary.autocomplete( 121 | ... 'Choose ant species', 122 | ... choices=[ 123 | ... 'Camponotus pennsylvanicus', 124 | ... 'Linepithema humile', 125 | ... 'Eciton burchellii', 126 | ... "Atta colombica", 127 | ... 'Polyergus lucidus', 128 | ... 'Polyergus rufescens', 129 | ... ]).ask() 130 | ? Choose ant species Atta colombica 131 | 'Atta colombica' 132 | 133 | .. image:: ../images/autocomplete.gif 134 | 135 | This is just a really basic example, the prompt can be customised using the 136 | parameters. 137 | 138 | 139 | Args: 140 | message: Question text 141 | 142 | choices: Items shown in the selection, this contains items as strings 143 | 144 | default: Default return value (single value). 145 | 146 | qmark: Question prefix displayed in front of the question. 147 | By default this is a ``?`` 148 | 149 | completer: A prompt_toolkit :class:`prompt_toolkit.completion.Completion` 150 | implementation. If not set, a questionary completer implementation 151 | will be used. 152 | 153 | meta_information: A dictionary with information/anything about choices. 154 | 155 | ignore_case: If true autocomplete would ignore case. 156 | 157 | match_middle: If true autocomplete would search in every string position 158 | not only in string begin. 159 | 160 | complete_style: How autocomplete menu would be shown, it could be ``COLUMN`` 161 | ``MULTI_COLUMN`` or ``READLINE_LIKE`` from 162 | :class:`prompt_toolkit.shortcuts.CompleteStyle`. 163 | 164 | validate: Require the entered value to pass a validation. The 165 | value can not be submitted until the validator accepts 166 | it (e.g. to check minimum password length). 167 | 168 | This can either be a function accepting the input and 169 | returning a boolean, or an class reference to a 170 | subclass of the prompt toolkit Validator class. 171 | 172 | style: A custom color and style for the question parts. You can 173 | configure colors as well as font types for different elements. 174 | 175 | Returns: 176 | :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). 177 | """ 178 | merged_style = merge_styles_default([style]) 179 | 180 | def get_prompt_tokens() -> List[Tuple[str, str]]: 181 | return [("class:qmark", qmark), ("class:question", " {} ".format(message))] 182 | 183 | def get_meta_style(meta: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: 184 | if meta: 185 | for key in meta: 186 | meta[key] = HTML("{}").format(meta[key]) 187 | 188 | return meta 189 | 190 | validator = build_validator(validate) 191 | 192 | if completer is None: 193 | if not choices: 194 | raise ValueError("No choices is given, you should use Text question.") 195 | # use the default completer 196 | completer = WordCompleter( 197 | choices, 198 | ignore_case=ignore_case, 199 | meta_information=get_meta_style(meta_information), 200 | match_middle=match_middle, 201 | ) 202 | 203 | p: PromptSession = PromptSession( 204 | get_prompt_tokens, 205 | lexer=SimpleLexer("class:answer"), 206 | style=merged_style, 207 | completer=completer, 208 | validator=validator, 209 | complete_style=complete_style, 210 | **kwargs, 211 | ) 212 | p.default_buffer.reset(Document(default)) 213 | 214 | return Question(p.app) 215 | -------------------------------------------------------------------------------- /questionary/prompts/confirm.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from typing import Optional 3 | 4 | from prompt_toolkit import PromptSession 5 | from prompt_toolkit.formatted_text import to_formatted_text 6 | from prompt_toolkit.key_binding import KeyBindings 7 | from prompt_toolkit.keys import Keys 8 | from prompt_toolkit.styles import Style 9 | 10 | from questionary.constants import DEFAULT_QUESTION_PREFIX 11 | from questionary.constants import NO 12 | from questionary.constants import NO_OR_YES 13 | from questionary.constants import YES 14 | from questionary.constants import YES_OR_NO 15 | from questionary.question import Question 16 | from questionary.styles import merge_styles_default 17 | 18 | 19 | def confirm( 20 | message: str, 21 | default: bool = True, 22 | qmark: str = DEFAULT_QUESTION_PREFIX, 23 | style: Optional[Style] = None, 24 | auto_enter: bool = True, 25 | instruction: Optional[str] = None, 26 | **kwargs: Any, 27 | ) -> Question: 28 | """A yes or no question. The user can either confirm or deny. 29 | 30 | This question type can be used to prompt the user for a confirmation 31 | of a yes-or-no question. If the user just hits enter, the default 32 | value will be returned. 33 | 34 | Example: 35 | >>> import questionary 36 | >>> questionary.confirm("Are you amazed?").ask() 37 | ? Are you amazed? Yes 38 | True 39 | 40 | .. image:: ../images/confirm.gif 41 | 42 | This is just a really basic example, the prompt can be customised using the 43 | parameters. 44 | 45 | 46 | Args: 47 | message: Question text. 48 | 49 | default: Default value will be returned if the user just hits 50 | enter. 51 | 52 | qmark: Question prefix displayed in front of the question. 53 | By default this is a ``?``. 54 | 55 | style: A custom color and style for the question parts. You can 56 | configure colors as well as font types for different elements. 57 | 58 | auto_enter: If set to `False`, the user needs to press the 'enter' key to 59 | accept their answer. If set to `True`, a valid input will be 60 | accepted without the need to press 'Enter'. 61 | 62 | instruction: A message describing how to proceed through the 63 | confirmation prompt. 64 | Returns: 65 | :class:`Question`: Question instance, ready to be prompted (using `.ask()`). 66 | """ 67 | merged_style = merge_styles_default([style]) 68 | 69 | status = {"answer": None, "complete": False} 70 | 71 | def get_prompt_tokens(): 72 | tokens = [] 73 | 74 | tokens.append(("class:qmark", qmark)) 75 | tokens.append(("class:question", " {} ".format(message))) 76 | 77 | if instruction is not None: 78 | tokens.append(("class:instruction", instruction)) 79 | elif not status["complete"]: 80 | _instruction = YES_OR_NO if default else NO_OR_YES 81 | tokens.append(("class:instruction", "{} ".format(_instruction))) 82 | 83 | if status["answer"] is not None: 84 | answer = YES if status["answer"] else NO 85 | tokens.append(("class:answer", answer)) 86 | 87 | return to_formatted_text(tokens) 88 | 89 | def exit_with_result(event): 90 | status["complete"] = True 91 | event.app.exit(result=status["answer"]) 92 | 93 | bindings = KeyBindings() 94 | 95 | @bindings.add(Keys.ControlQ, eager=True) 96 | @bindings.add(Keys.ControlC, eager=True) 97 | def _(event): 98 | event.app.exit(exception=KeyboardInterrupt, style="class:aborting") 99 | 100 | @bindings.add("n") 101 | @bindings.add("N") 102 | def key_n(event): 103 | status["answer"] = False 104 | if auto_enter: 105 | exit_with_result(event) 106 | 107 | @bindings.add("y") 108 | @bindings.add("Y") 109 | def key_y(event): 110 | status["answer"] = True 111 | if auto_enter: 112 | exit_with_result(event) 113 | 114 | @bindings.add(Keys.ControlH) 115 | def key_backspace(event): 116 | status["answer"] = None 117 | 118 | @bindings.add(Keys.ControlM, eager=True) 119 | def set_answer(event): 120 | if status["answer"] is None: 121 | status["answer"] = default 122 | 123 | exit_with_result(event) 124 | 125 | @bindings.add(Keys.Any) 126 | def other(event): 127 | """Disallow inserting other text.""" 128 | 129 | return Question( 130 | PromptSession( 131 | get_prompt_tokens, key_bindings=bindings, style=merged_style, **kwargs 132 | ).app 133 | ) 134 | -------------------------------------------------------------------------------- /questionary/prompts/password.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from typing import Optional 3 | 4 | from questionary import Style 5 | from questionary.constants import DEFAULT_QUESTION_PREFIX 6 | from questionary.prompts import text 7 | from questionary.question import Question 8 | 9 | 10 | def password( 11 | message: str, 12 | default: str = "", 13 | validate: Any = None, 14 | qmark: str = DEFAULT_QUESTION_PREFIX, 15 | style: Optional[Style] = None, 16 | **kwargs: Any, 17 | ) -> Question: 18 | """A text input where a user can enter a secret which won't be displayed on the CLI. 19 | 20 | This question type can be used to prompt the user for information 21 | that should not be shown in the command line. The typed text will be 22 | replaced with ``*``. 23 | 24 | Example: 25 | >>> import questionary 26 | >>> questionary.password("What's your secret?").ask() 27 | ? What's your secret? ******** 28 | 'secret42' 29 | 30 | .. image:: ../images/password.gif 31 | 32 | This is just a really basic example, the prompt can be customised using the 33 | parameters. 34 | 35 | Args: 36 | message: Question text. 37 | 38 | default: Default value will be returned if the user just hits 39 | enter. 40 | 41 | validate: Require the entered value to pass a validation. The 42 | value can not be submitted until the validator accepts 43 | it (e.g. to check minimum password length). 44 | 45 | This can either be a function accepting the input and 46 | returning a boolean, or an class reference to a 47 | subclass of the prompt toolkit Validator class. 48 | 49 | qmark: Question prefix displayed in front of the question. 50 | By default this is a ``?``. 51 | 52 | style: A custom color and style for the question parts. You can 53 | configure colors as well as font types for different elements. 54 | 55 | Returns: 56 | :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). 57 | """ 58 | 59 | return text.text( 60 | message, default, validate, qmark, style, is_password=True, **kwargs 61 | ) 62 | -------------------------------------------------------------------------------- /questionary/prompts/path.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any 3 | from typing import Callable 4 | from typing import Iterable 5 | from typing import List 6 | from typing import Optional 7 | from typing import Tuple 8 | 9 | from prompt_toolkit.completion import CompleteEvent 10 | from prompt_toolkit.completion import Completion 11 | from prompt_toolkit.completion import PathCompleter 12 | from prompt_toolkit.completion.base import Completer 13 | from prompt_toolkit.document import Document 14 | from prompt_toolkit.key_binding import KeyBindings 15 | from prompt_toolkit.key_binding.key_processor import KeyPressEvent 16 | from prompt_toolkit.keys import Keys 17 | from prompt_toolkit.lexers import SimpleLexer 18 | from prompt_toolkit.shortcuts.prompt import CompleteStyle 19 | from prompt_toolkit.shortcuts.prompt import PromptSession 20 | from prompt_toolkit.styles import Style 21 | 22 | from questionary.constants import DEFAULT_QUESTION_PREFIX 23 | from questionary.prompts.common import build_validator 24 | from questionary.question import Question 25 | from questionary.styles import merge_styles_default 26 | 27 | 28 | class GreatUXPathCompleter(PathCompleter): 29 | """Wraps :class:`prompt_toolkit.completion.PathCompleter`. 30 | 31 | Makes sure completions for directories end with a path separator. Also make sure 32 | the right path separator is used. Checks if `get_paths` returns list of existing 33 | directories. 34 | """ 35 | 36 | def __init__( 37 | self, 38 | only_directories: bool = False, 39 | get_paths: Optional[Callable[[], List[str]]] = None, 40 | file_filter: Optional[Callable[[str], bool]] = None, 41 | min_input_len: int = 0, 42 | expanduser: bool = False, 43 | ) -> None: 44 | """Adds validation of 'get_paths' to :class:`prompt_toolkit.completion.PathCompleter`. 45 | 46 | Args: 47 | only_directories (bool): If True, only directories will be 48 | returned, but no files. Defaults to False. 49 | get_paths (Callable[[], List[str]], optional): Callable which 50 | returns a list of directories to look into when the user enters a 51 | relative path. If None, set to (lambda: ["."]). Defaults to None. 52 | file_filter (Callable[[str], bool], optional): Callable which 53 | takes a filename and returns whether this file should show up in the 54 | completion. ``None`` when no filtering has to be done. Defaults to None. 55 | min_input_len (int): Don't do autocompletion when the input string 56 | is shorter. Defaults to 0. 57 | expanduser (bool): If True, tilde (~) is expanded. Defaults to 58 | False. 59 | 60 | Raises: 61 | ValueError: If any of the by `get_paths` returned directories does not 62 | exist. 63 | """ 64 | # if get_paths is None, make it return the current working dir 65 | get_paths = get_paths or (lambda: ["."]) 66 | # validation of get_paths 67 | for current_path in get_paths(): 68 | if not os.path.isdir(current_path): 69 | raise ( 70 | ValueError( 71 | "\n Completer for file paths 'get_paths' must return only existing directories, but" 72 | f" '{current_path}' does not exist." 73 | ) 74 | ) 75 | # call PathCompleter __init__ 76 | super().__init__( 77 | only_directories=only_directories, 78 | get_paths=get_paths, 79 | file_filter=file_filter, 80 | min_input_len=min_input_len, 81 | expanduser=expanduser, 82 | ) 83 | 84 | def get_completions( 85 | self, document: Document, complete_event: CompleteEvent 86 | ) -> Iterable[Completion]: 87 | """Get completions. 88 | 89 | Wraps :class:`prompt_toolkit.completion.PathCompleter`. Makes sure completions 90 | for directories end with a path separator. Also make sure the right path 91 | separator is used. 92 | """ 93 | completions = super(GreatUXPathCompleter, self).get_completions( 94 | document, complete_event 95 | ) 96 | 97 | for completion in completions: 98 | # check if the display value ends with a path separator. 99 | # first check if display is properly set 100 | styled_display = completion.display[0] 101 | # styled display is a formatted text (a tuple of the text and its style) 102 | # second tuple entry is the text 103 | if styled_display[1][-1] == "/": 104 | # replace separator with the OS specific one 105 | display_text = styled_display[1][:-1] + os.path.sep 106 | # update the styled display with the modified text 107 | completion.display[0] = (styled_display[0], display_text) 108 | # append the separator to the text as well - unclear why the normal 109 | # path completer omits it from the text. this improves UX for the 110 | # user, as they don't need to type the separator after auto-completing 111 | # a directory 112 | completion.text += os.path.sep 113 | yield completion 114 | 115 | 116 | def path( 117 | message: str, 118 | default: str = "", 119 | qmark: str = DEFAULT_QUESTION_PREFIX, 120 | validate: Any = None, 121 | completer: Optional[Completer] = None, 122 | style: Optional[Style] = None, 123 | only_directories: bool = False, 124 | get_paths: Optional[Callable[[], List[str]]] = None, 125 | file_filter: Optional[Callable[[str], bool]] = None, 126 | complete_style: CompleteStyle = CompleteStyle.MULTI_COLUMN, 127 | **kwargs: Any, 128 | ) -> Question: 129 | """A text input for a file or directory path with autocompletion enabled. 130 | 131 | Example: 132 | >>> import questionary 133 | >>> questionary.path( 134 | >>> "What's the path to the projects version file?" 135 | >>> ).ask() 136 | ? What's the path to the projects version file? ./pyproject.toml 137 | './pyproject.toml' 138 | 139 | .. image:: ../images/path.gif 140 | 141 | This is just a really basic example, the prompt can be customized using the 142 | parameters. 143 | 144 | Args: 145 | message: Question text. 146 | 147 | default: Default return value (single value). 148 | 149 | qmark: Question prefix displayed in front of the question. 150 | By default this is a ``?``. 151 | 152 | complete_style: How autocomplete menu would be shown, it could be ``COLUMN`` 153 | ``MULTI_COLUMN`` or ``READLINE_LIKE`` from 154 | :class:`prompt_toolkit.shortcuts.CompleteStyle`. 155 | 156 | validate: Require the entered value to pass a validation. The 157 | value can not be submitted until the validator accepts 158 | it (e.g. to check minimum password length). 159 | 160 | This can either be a function accepting the input and 161 | returning a boolean, or an class reference to a 162 | subclass of the prompt toolkit Validator class. 163 | 164 | completer: A custom completer to use in the prompt. For more information, 165 | see `this `_. 166 | 167 | style: A custom color and style for the question parts. You can 168 | configure colors as well as font types for different elements. 169 | 170 | only_directories: Only show directories in auto completion. This option 171 | does not do anything if a custom ``completer`` is 172 | passed. 173 | 174 | get_paths: Set a callable to generate paths to traverse for suggestions. This option 175 | does not do anything if a custom ``completer`` is 176 | passed. 177 | 178 | file_filter: Optional callable to filter suggested paths. Only paths 179 | where the passed callable evaluates to ``True`` will show up in 180 | the suggested paths. This does not validate the typed path, e.g. 181 | it is still possible for the user to enter a path manually, even 182 | though this filter evaluates to ``False``. If in addition to 183 | filtering suggestions you also want to validate the result, use 184 | ``validate`` in combination with the ``file_filter``. 185 | 186 | Returns: 187 | :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). 188 | """ # noqa: W505, E501 189 | merged_style = merge_styles_default([style]) 190 | 191 | def get_prompt_tokens() -> List[Tuple[str, str]]: 192 | return [("class:qmark", qmark), ("class:question", " {} ".format(message))] 193 | 194 | validator = build_validator(validate) 195 | 196 | completer = completer or GreatUXPathCompleter( 197 | get_paths=get_paths, 198 | only_directories=only_directories, 199 | file_filter=file_filter, 200 | expanduser=True, 201 | ) 202 | 203 | bindings = KeyBindings() 204 | 205 | @bindings.add(Keys.ControlM, eager=True) 206 | def set_answer(event: KeyPressEvent): 207 | if event.current_buffer.complete_state is not None: 208 | event.current_buffer.complete_state = None 209 | elif event.app.current_buffer.validate(set_cursor=True): 210 | # When the validation succeeded, accept the input. 211 | result_path = event.app.current_buffer.document.text 212 | if result_path.endswith(os.path.sep): 213 | result_path = result_path[:-1] 214 | 215 | event.app.exit(result=result_path) 216 | event.app.current_buffer.append_to_history() 217 | 218 | @bindings.add(os.path.sep, eager=True) 219 | def next_segment(event: KeyPressEvent): 220 | b = event.app.current_buffer 221 | 222 | if b.complete_state: 223 | b.complete_state = None 224 | 225 | current_path = b.document.text 226 | if not current_path.endswith(os.path.sep): 227 | b.insert_text(os.path.sep) 228 | 229 | b.start_completion(select_first=False) 230 | 231 | p: PromptSession = PromptSession( 232 | get_prompt_tokens, 233 | lexer=SimpleLexer("class:answer"), 234 | style=merged_style, 235 | completer=completer, 236 | validator=validator, 237 | complete_style=complete_style, 238 | key_bindings=bindings, 239 | **kwargs, 240 | ) 241 | p.default_buffer.reset(Document(default)) 242 | 243 | return Question(p.app) 244 | -------------------------------------------------------------------------------- /questionary/prompts/press_any_key_to_continue.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from typing import Optional 3 | 4 | from prompt_toolkit import PromptSession 5 | from prompt_toolkit.formatted_text import to_formatted_text 6 | from prompt_toolkit.key_binding import KeyBindings 7 | from prompt_toolkit.keys import Keys 8 | from prompt_toolkit.styles import Style 9 | 10 | from questionary.question import Question 11 | from questionary.styles import merge_styles_default 12 | 13 | 14 | def press_any_key_to_continue( 15 | message: Optional[str] = None, 16 | style: Optional[Style] = None, 17 | **kwargs: Any, 18 | ): 19 | """Wait until user presses any key to continue. 20 | 21 | Example: 22 | >>> import questionary 23 | >>> questionary.press_any_key_to_continue().ask() 24 | Press any key to continue... 25 | '' 26 | 27 | Args: 28 | message: Question text. Defaults to ``"Press any key to continue..."`` 29 | 30 | style: A custom color and style for the question parts. You can 31 | configure colors as well as font types for different elements. 32 | 33 | Returns: 34 | :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). 35 | """ 36 | merged_style = merge_styles_default([style]) 37 | 38 | if message is None: 39 | message = "Press any key to continue..." 40 | 41 | def get_prompt_tokens(): 42 | tokens = [] 43 | 44 | tokens.append(("class:question", f" {message} ")) 45 | 46 | return to_formatted_text(tokens) 47 | 48 | def exit_with_result(event): 49 | event.app.exit(result=None) 50 | 51 | bindings = KeyBindings() 52 | 53 | @bindings.add(Keys.Any) 54 | def any_key(event): 55 | exit_with_result(event) 56 | 57 | return Question( 58 | PromptSession( 59 | get_prompt_tokens, key_bindings=bindings, style=merged_style, **kwargs 60 | ).app 61 | ) 62 | -------------------------------------------------------------------------------- /questionary/prompts/rawselect.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from typing import Dict 3 | from typing import Optional 4 | from typing import Sequence 5 | from typing import Union 6 | 7 | from prompt_toolkit.styles import Style 8 | 9 | from questionary.constants import DEFAULT_QUESTION_PREFIX 10 | from questionary.constants import DEFAULT_SELECTED_POINTER 11 | from questionary.prompts import select 12 | from questionary.prompts.common import Choice 13 | from questionary.question import Question 14 | 15 | 16 | def rawselect( 17 | message: str, 18 | choices: Sequence[Union[str, Choice, Dict[str, Any]]], 19 | default: Optional[str] = None, 20 | qmark: str = DEFAULT_QUESTION_PREFIX, 21 | pointer: Optional[str] = DEFAULT_SELECTED_POINTER, 22 | style: Optional[Style] = None, 23 | **kwargs: Any, 24 | ) -> Question: 25 | """Ask the user to select one item from a list of choices using shortcuts. 26 | 27 | The user can only select one option. 28 | 29 | Example: 30 | >>> import questionary 31 | >>> questionary.rawselect( 32 | ... "What do you want to do?", 33 | ... choices=[ 34 | ... "Order a pizza", 35 | ... "Make a reservation", 36 | ... "Ask for opening hours" 37 | ... ]).ask() 38 | ? What do you want to do? Order a pizza 39 | 'Order a pizza' 40 | 41 | .. image:: ../images/rawselect.gif 42 | 43 | This is just a really basic example, the prompt can be customised using the 44 | parameters. 45 | 46 | Args: 47 | message: Question text. 48 | 49 | choices: Items shown in the selection, this can contain :class:`Choice` or 50 | or :class:`Separator` objects or simple items as strings. Passing 51 | :class:`Choice` objects, allows you to configure the item more 52 | (e.g. preselecting it or disabling it). 53 | 54 | default: Default return value (single value). 55 | 56 | qmark: Question prefix displayed in front of the question. 57 | By default this is a ``?``. 58 | 59 | pointer: Pointer symbol in front of the currently highlighted element. 60 | By default this is a ``»``. 61 | Use ``None`` to disable it. 62 | 63 | style: A custom color and style for the question parts. You can 64 | configure colors as well as font types for different elements. 65 | 66 | Returns: 67 | :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). 68 | """ 69 | return select.select( 70 | message, 71 | choices, 72 | default, 73 | qmark, 74 | pointer, 75 | style, 76 | use_shortcuts=True, 77 | use_arrow_keys=False, 78 | **kwargs, 79 | ) 80 | -------------------------------------------------------------------------------- /questionary/prompts/select.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import string 4 | from typing import Any 5 | from typing import Dict 6 | from typing import Optional 7 | from typing import Sequence 8 | from typing import Union 9 | 10 | from prompt_toolkit.application import Application 11 | from prompt_toolkit.key_binding import KeyBindings 12 | from prompt_toolkit.keys import Keys 13 | from prompt_toolkit.styles import Style 14 | 15 | from questionary import utils 16 | from questionary.constants import DEFAULT_QUESTION_PREFIX 17 | from questionary.constants import DEFAULT_SELECTED_POINTER 18 | from questionary.prompts import common 19 | from questionary.prompts.common import Choice 20 | from questionary.prompts.common import InquirerControl 21 | from questionary.prompts.common import Separator 22 | from questionary.question import Question 23 | from questionary.styles import merge_styles_default 24 | 25 | 26 | def select( 27 | message: str, 28 | choices: Sequence[Union[str, Choice, Dict[str, Any]]], 29 | default: Optional[Union[str, Choice, Dict[str, Any]]] = None, 30 | qmark: str = DEFAULT_QUESTION_PREFIX, 31 | pointer: Optional[str] = DEFAULT_SELECTED_POINTER, 32 | style: Optional[Style] = None, 33 | use_shortcuts: bool = False, 34 | use_arrow_keys: bool = True, 35 | use_indicator: bool = False, 36 | use_jk_keys: bool = True, 37 | use_emacs_keys: bool = True, 38 | use_search_filter: bool = False, 39 | show_selected: bool = False, 40 | show_description: bool = True, 41 | instruction: Optional[str] = None, 42 | **kwargs: Any, 43 | ) -> Question: 44 | """A list of items to select **one** option from. 45 | 46 | The user can pick one option and confirm it (if you want to allow 47 | the user to select multiple options, use :meth:`questionary.checkbox` instead). 48 | 49 | Example: 50 | >>> import questionary 51 | >>> questionary.select( 52 | ... "What do you want to do?", 53 | ... choices=[ 54 | ... "Order a pizza", 55 | ... "Make a reservation", 56 | ... "Ask for opening hours" 57 | ... ]).ask() 58 | ? What do you want to do? Order a pizza 59 | 'Order a pizza' 60 | 61 | .. image:: ../images/select.gif 62 | 63 | This is just a really basic example, the prompt can be customised using the 64 | parameters. 65 | 66 | 67 | Args: 68 | message: Question text 69 | 70 | choices: Items shown in the selection, this can contain :class:`Choice` or 71 | or :class:`Separator` objects or simple items as strings. Passing 72 | :class:`Choice` objects, allows you to configure the item more 73 | (e.g. preselecting it or disabling it). 74 | 75 | default: A value corresponding to a selectable item in the choices, 76 | to initially set the pointer position to. 77 | 78 | qmark: Question prefix displayed in front of the question. 79 | By default this is a ``?``. 80 | 81 | pointer: Pointer symbol in front of the currently highlighted element. 82 | By default this is a ``»``. 83 | Use ``None`` to disable it. 84 | 85 | instruction: A hint on how to navigate the menu. 86 | It's ``(Use shortcuts)`` if only ``use_shortcuts`` is set 87 | to True, ``(Use arrow keys or shortcuts)`` if ``use_arrow_keys`` 88 | & ``use_shortcuts`` are set and ``(Use arrow keys)`` by default. 89 | 90 | style: A custom color and style for the question parts. You can 91 | configure colors as well as font types for different elements. 92 | 93 | use_indicator: Flag to enable the small indicator in front of the 94 | list highlighting the current location of the selection 95 | cursor. 96 | 97 | use_shortcuts: Allow the user to select items from the list using 98 | shortcuts. The shortcuts will be displayed in front of 99 | the list items. Arrow keys, j/k keys and shortcuts are 100 | not mutually exclusive. 101 | 102 | use_arrow_keys: Allow the user to select items from the list using 103 | arrow keys. Arrow keys, j/k keys and shortcuts are not 104 | mutually exclusive. 105 | 106 | use_jk_keys: Allow the user to select items from the list using 107 | `j` (down) and `k` (up) keys. Arrow keys, j/k keys and 108 | shortcuts are not mutually exclusive. 109 | 110 | use_emacs_keys: Allow the user to select items from the list using 111 | `Ctrl+N` (down) and `Ctrl+P` (up) keys. Arrow keys, j/k keys, 112 | emacs keys and shortcuts are not mutually exclusive. 113 | 114 | use_search_filter: Flag to enable search filtering. Typing some string will 115 | filter the choices to keep only the ones that contain the 116 | search string. 117 | Note that activating this option disables "vi-like" 118 | navigation as "j" and "k" can be part of a prefix and 119 | therefore cannot be used for navigation 120 | 121 | show_selected: Display current selection choice at the bottom of list. 122 | 123 | show_description: Display description of current selection if available. 124 | 125 | Returns: 126 | :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). 127 | """ 128 | if not (use_arrow_keys or use_shortcuts or use_jk_keys or use_emacs_keys): 129 | raise ValueError( 130 | ( 131 | "Some option to move the selection is required. " 132 | "Arrow keys, j/k keys, emacs keys, or shortcuts." 133 | ) 134 | ) 135 | 136 | if use_jk_keys and use_search_filter: 137 | raise ValueError( 138 | "Cannot use j/k keys with prefix filter search, since j/k can be part of the prefix." 139 | ) 140 | 141 | if use_shortcuts and use_jk_keys: 142 | if any(getattr(c, "shortcut_key", "") in ["j", "k"] for c in choices): 143 | raise ValueError( 144 | "A choice is trying to register j/k as a " 145 | "shortcut key when they are in use as arrow keys " 146 | "disable one or the other." 147 | ) 148 | 149 | if choices is None or len(choices) == 0: 150 | raise ValueError("A list of choices needs to be provided.") 151 | 152 | if use_shortcuts: 153 | real_len_of_choices = sum(1 for c in choices if not isinstance(c, Separator)) 154 | if real_len_of_choices > len(InquirerControl.SHORTCUT_KEYS): 155 | raise ValueError( 156 | "A list with shortcuts supports a maximum of {} " 157 | "choices as this is the maximum number " 158 | "of keyboard shortcuts that are available. You " 159 | "provided {} choices!" 160 | "".format(len(InquirerControl.SHORTCUT_KEYS), real_len_of_choices) 161 | ) 162 | 163 | merged_style = merge_styles_default([style]) 164 | 165 | ic = InquirerControl( 166 | choices, 167 | default, 168 | pointer=pointer, 169 | use_indicator=use_indicator, 170 | use_shortcuts=use_shortcuts, 171 | show_selected=show_selected, 172 | show_description=show_description, 173 | use_arrow_keys=use_arrow_keys, 174 | initial_choice=default, 175 | ) 176 | 177 | def get_prompt_tokens(): 178 | # noinspection PyListCreation 179 | tokens = [("class:qmark", qmark), ("class:question", " {} ".format(message))] 180 | 181 | if ic.is_answered: 182 | if isinstance(ic.get_pointed_at().title, list): 183 | tokens.append( 184 | ( 185 | "class:answer", 186 | "".join([token[1] for token in ic.get_pointed_at().title]), 187 | ) 188 | ) 189 | else: 190 | tokens.append(("class:answer", ic.get_pointed_at().title)) 191 | else: 192 | if instruction: 193 | tokens.append(("class:instruction", instruction)) 194 | else: 195 | if use_shortcuts and use_arrow_keys: 196 | instruction_msg = f"(Use shortcuts or arrow keys{', type to filter' if use_search_filter else ''})" 197 | elif use_shortcuts and not use_arrow_keys: 198 | instruction_msg = f"(Use shortcuts{', type to filter' if use_search_filter else ''})" 199 | else: 200 | instruction_msg = f"(Use arrow keys{', type to filter' if use_search_filter else ''})" 201 | tokens.append(("class:instruction", instruction_msg)) 202 | 203 | return tokens 204 | 205 | layout = common.create_inquirer_layout(ic, get_prompt_tokens, **kwargs) 206 | 207 | bindings = KeyBindings() 208 | 209 | @bindings.add(Keys.ControlQ, eager=True) 210 | @bindings.add(Keys.ControlC, eager=True) 211 | def _(event): 212 | event.app.exit(exception=KeyboardInterrupt, style="class:aborting") 213 | 214 | if use_shortcuts: 215 | # add key bindings for choices 216 | for i, c in enumerate(ic.choices): 217 | if c.shortcut_key is None and not c.disabled and not use_arrow_keys: 218 | raise RuntimeError( 219 | "{} does not have a shortcut and arrow keys " 220 | "for movement are disabled. " 221 | "This choice is not reachable.".format(c.title) 222 | ) 223 | if isinstance(c, Separator) or c.shortcut_key is None or c.disabled: 224 | continue 225 | 226 | # noinspection PyShadowingNames 227 | def _reg_binding(i, keys): 228 | # trick out late evaluation with a "function factory": 229 | # https://stackoverflow.com/a/3431699 230 | @bindings.add(keys, eager=True) 231 | def select_choice(event): 232 | ic.pointed_at = i 233 | 234 | _reg_binding(i, c.shortcut_key) 235 | 236 | def move_cursor_down(event): 237 | ic.select_next() 238 | while not ic.is_selection_valid(): 239 | ic.select_next() 240 | 241 | def move_cursor_up(event): 242 | ic.select_previous() 243 | while not ic.is_selection_valid(): 244 | ic.select_previous() 245 | 246 | if use_search_filter: 247 | 248 | def search_filter(event): 249 | ic.add_search_character(event.key_sequence[0].key) 250 | 251 | for character in string.printable: 252 | bindings.add(character, eager=True)(search_filter) 253 | bindings.add(Keys.Backspace, eager=True)(search_filter) 254 | 255 | if use_arrow_keys: 256 | bindings.add(Keys.Down, eager=True)(move_cursor_down) 257 | bindings.add(Keys.Up, eager=True)(move_cursor_up) 258 | 259 | if use_jk_keys: 260 | bindings.add("j", eager=True)(move_cursor_down) 261 | bindings.add("k", eager=True)(move_cursor_up) 262 | 263 | if use_emacs_keys: 264 | bindings.add(Keys.ControlN, eager=True)(move_cursor_down) 265 | bindings.add(Keys.ControlP, eager=True)(move_cursor_up) 266 | 267 | @bindings.add(Keys.ControlM, eager=True) 268 | def set_answer(event): 269 | ic.is_answered = True 270 | event.app.exit(result=ic.get_pointed_at().value) 271 | 272 | @bindings.add(Keys.Any) 273 | def other(event): 274 | """Disallow inserting other text.""" 275 | 276 | return Question( 277 | Application( 278 | layout=layout, 279 | key_bindings=bindings, 280 | style=merged_style, 281 | **utils.used_kwargs(kwargs, Application.__init__), 282 | ) 283 | ) 284 | -------------------------------------------------------------------------------- /questionary/prompts/text.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from typing import List 3 | from typing import Optional 4 | from typing import Tuple 5 | 6 | from prompt_toolkit.document import Document 7 | from prompt_toolkit.lexers import Lexer 8 | from prompt_toolkit.lexers import SimpleLexer 9 | from prompt_toolkit.shortcuts.prompt import PromptSession 10 | from prompt_toolkit.styles import Style 11 | 12 | from questionary.constants import DEFAULT_QUESTION_PREFIX 13 | from questionary.constants import INSTRUCTION_MULTILINE 14 | from questionary.prompts.common import build_validator 15 | from questionary.question import Question 16 | from questionary.styles import merge_styles_default 17 | 18 | 19 | def text( 20 | message: str, 21 | default: str = "", 22 | validate: Any = None, 23 | qmark: str = DEFAULT_QUESTION_PREFIX, 24 | style: Optional[Style] = None, 25 | multiline: bool = False, 26 | instruction: Optional[str] = None, 27 | lexer: Optional[Lexer] = None, 28 | **kwargs: Any, 29 | ) -> Question: 30 | """Prompt the user to enter a free text message. 31 | 32 | This question type can be used to prompt the user for some text input. 33 | 34 | Example: 35 | >>> import questionary 36 | >>> questionary.text("What's your first name?").ask() 37 | ? What's your first name? Tom 38 | 'Tom' 39 | 40 | .. image:: ../images/text.gif 41 | 42 | This is just a really basic example, the prompt can be customised using the 43 | parameters. 44 | 45 | Args: 46 | message: Question text. 47 | 48 | default: Default value will be returned if the user just hits 49 | enter. 50 | 51 | validate: Require the entered value to pass a validation. The 52 | value can not be submitted until the validator accepts 53 | it (e.g. to check minimum password length). 54 | 55 | This can either be a function accepting the input and 56 | returning a boolean, or an class reference to a 57 | subclass of the prompt toolkit Validator class. 58 | 59 | qmark: Question prefix displayed in front of the question. 60 | By default this is a ``?``. 61 | 62 | style: A custom color and style for the question parts. You can 63 | configure colors as well as font types for different elements. 64 | 65 | multiline: If ``True``, multiline input will be enabled. 66 | 67 | instruction: Write instructions for the user if needed. If ``None`` 68 | and ``multiline=True``, some instructions will appear. 69 | 70 | lexer: Supply a valid lexer to style the answer. Leave empty to 71 | use a simple one by default. 72 | 73 | kwargs: Additional arguments, they will be passed to prompt toolkit. 74 | 75 | Returns: 76 | :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). 77 | """ 78 | merged_style = merge_styles_default([style]) 79 | lexer = lexer or SimpleLexer("class:answer") 80 | validator = build_validator(validate) 81 | 82 | if instruction is None and multiline: 83 | instruction = INSTRUCTION_MULTILINE 84 | 85 | def get_prompt_tokens() -> List[Tuple[str, str]]: 86 | result = [("class:qmark", qmark), ("class:question", " {} ".format(message))] 87 | if instruction: 88 | result.append(("class:instruction", " {} ".format(instruction))) 89 | return result 90 | 91 | p: PromptSession = PromptSession( 92 | get_prompt_tokens, 93 | style=merged_style, 94 | validator=validator, 95 | lexer=lexer, 96 | multiline=multiline, 97 | **kwargs, 98 | ) 99 | p.default_buffer.reset(Document(default)) 100 | 101 | return Question(p.app) 102 | -------------------------------------------------------------------------------- /questionary/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/questionary/py.typed -------------------------------------------------------------------------------- /questionary/question.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Any 3 | 4 | import prompt_toolkit.patch_stdout 5 | from prompt_toolkit import Application 6 | 7 | from questionary import utils 8 | from questionary.constants import DEFAULT_KBI_MESSAGE 9 | 10 | 11 | class Question: 12 | """A question to be prompted. 13 | 14 | This is an internal class. Questions should be created using the 15 | predefined questions (e.g. text or password).""" 16 | 17 | application: "Application[Any]" 18 | should_skip_question: bool 19 | default: Any 20 | 21 | def __init__(self, application: "Application[Any]") -> None: 22 | self.application = application 23 | self.should_skip_question = False 24 | self.default = None 25 | 26 | async def ask_async( 27 | self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE 28 | ) -> Any: 29 | """Ask the question using asyncio and return user response. 30 | 31 | Args: 32 | patch_stdout: Ensure that the prompt renders correctly if other threads 33 | are printing to stdout. 34 | 35 | kbi_msg: The message to be printed on a keyboard interrupt. 36 | 37 | Returns: 38 | `Any`: The answer from the question. 39 | """ 40 | 41 | try: 42 | sys.stdout.flush() 43 | return await self.unsafe_ask_async(patch_stdout) 44 | except KeyboardInterrupt: 45 | print("{}".format(kbi_msg)) 46 | return None 47 | 48 | def ask( 49 | self, patch_stdout: bool = False, kbi_msg: str = DEFAULT_KBI_MESSAGE 50 | ) -> Any: 51 | """Ask the question synchronously and return user response. 52 | 53 | Args: 54 | patch_stdout: Ensure that the prompt renders correctly if other threads 55 | are printing to stdout. 56 | 57 | kbi_msg: The message to be printed on a keyboard interrupt. 58 | 59 | Returns: 60 | `Any`: The answer from the question. 61 | """ 62 | 63 | try: 64 | return self.unsafe_ask(patch_stdout) 65 | except KeyboardInterrupt: 66 | print("{}".format(kbi_msg)) 67 | return None 68 | 69 | def unsafe_ask(self, patch_stdout: bool = False) -> Any: 70 | """Ask the question synchronously and return user response. 71 | 72 | Does not catch keyboard interrupts. 73 | 74 | Args: 75 | patch_stdout: Ensure that the prompt renders correctly if other threads 76 | are printing to stdout. 77 | 78 | Returns: 79 | `Any`: The answer from the question. 80 | """ 81 | 82 | if self.should_skip_question: 83 | return self.default 84 | 85 | if patch_stdout: 86 | with prompt_toolkit.patch_stdout.patch_stdout(): 87 | return self.application.run() 88 | else: 89 | return self.application.run() 90 | 91 | def skip_if(self, condition: bool, default: Any = None) -> "Question": 92 | """Skip the question if flag is set and return the default instead. 93 | 94 | Args: 95 | condition: A conditional boolean value. 96 | default: The default value to return. 97 | 98 | Returns: 99 | :class:`Question`: `self`. 100 | """ 101 | 102 | self.should_skip_question = condition 103 | self.default = default 104 | return self 105 | 106 | async def unsafe_ask_async(self, patch_stdout: bool = False) -> Any: 107 | """Ask the question using asyncio and return user response. 108 | 109 | Does not catch keyboard interrupts. 110 | 111 | Args: 112 | patch_stdout: Ensure that the prompt renders correctly if other threads 113 | are printing to stdout. 114 | 115 | Returns: 116 | `Any`: The answer from the question. 117 | """ 118 | 119 | if self.should_skip_question: 120 | return self.default 121 | 122 | if not utils.ACTIVATED_ASYNC_MODE: 123 | await utils.activate_prompt_toolkit_async_mode() 124 | 125 | if patch_stdout: 126 | with prompt_toolkit.patch_stdout.patch_stdout(): 127 | r = self.application.run_async() 128 | else: 129 | r = self.application.run_async() 130 | 131 | if utils.is_prompt_toolkit_3(): 132 | return await r 133 | else: 134 | return await r.to_asyncio_future() # type: ignore[attr-defined] 135 | -------------------------------------------------------------------------------- /questionary/styles.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from typing import Optional 3 | 4 | import prompt_toolkit.styles 5 | 6 | from questionary.constants import DEFAULT_STYLE 7 | 8 | 9 | def merge_styles_default(styles: List[Optional[prompt_toolkit.styles.Style]]): 10 | """Merge a list of styles with the Questionary default style.""" 11 | filtered_styles: list[prompt_toolkit.styles.BaseStyle] = [DEFAULT_STYLE] 12 | # prompt_toolkit's merge_styles works with ``None`` elements, but it's 13 | # type-hints says it doesn't. 14 | filtered_styles.extend([s for s in styles if s is not None]) 15 | return prompt_toolkit.styles.merge_styles(filtered_styles) 16 | -------------------------------------------------------------------------------- /questionary/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import Any 3 | from typing import Callable 4 | from typing import Dict 5 | from typing import List 6 | from typing import Set 7 | 8 | ACTIVATED_ASYNC_MODE = False 9 | 10 | 11 | def is_prompt_toolkit_3() -> bool: 12 | from prompt_toolkit import __version__ as ptk_version 13 | 14 | return ptk_version.startswith("3.") 15 | 16 | 17 | def default_values_of(func: Callable[..., Any]) -> List[str]: 18 | """Return all parameter names of ``func`` with a default value.""" 19 | 20 | signature = inspect.signature(func) 21 | return [ 22 | k 23 | for k, v in signature.parameters.items() 24 | if v.default is not inspect.Parameter.empty 25 | or v.kind != inspect.Parameter.POSITIONAL_OR_KEYWORD 26 | ] 27 | 28 | 29 | def arguments_of(func: Callable[..., Any]) -> List[str]: 30 | """Return the parameter names of the function ``func``.""" 31 | 32 | return list(inspect.signature(func).parameters.keys()) 33 | 34 | 35 | def used_kwargs(kwargs: Dict[str, Any], func: Callable[..., Any]) -> Dict[str, Any]: 36 | """Returns only the kwargs which can be used by a function. 37 | 38 | Args: 39 | kwargs: All available kwargs. 40 | func: The function which should be called. 41 | 42 | Returns: 43 | Subset of kwargs which are accepted by ``func``. 44 | """ 45 | 46 | possible_arguments = arguments_of(func) 47 | 48 | return {k: v for k, v in kwargs.items() if k in possible_arguments} 49 | 50 | 51 | def required_arguments(func: Callable[..., Any]) -> List[str]: 52 | """Return all arguments of a function that do not have a default value.""" 53 | defaults = default_values_of(func) 54 | args = arguments_of(func) 55 | 56 | if defaults: 57 | args = args[: -len(defaults)] 58 | return args # all args without default values 59 | 60 | 61 | def missing_arguments(func: Callable[..., Any], argdict: Dict[str, Any]) -> Set[str]: 62 | """Return all arguments that are missing to call func.""" 63 | return set(required_arguments(func)) - set(argdict.keys()) 64 | 65 | 66 | async def activate_prompt_toolkit_async_mode() -> None: 67 | """Configure prompt toolkit to use the asyncio event loop. 68 | 69 | Needs to be async, so we use the right event loop in py 3.5""" 70 | global ACTIVATED_ASYNC_MODE 71 | 72 | if not is_prompt_toolkit_3(): 73 | # Tell prompt_toolkit to use asyncio for the event loop. 74 | import prompt_toolkit as pt 75 | 76 | pt.eventloop.use_asyncio_event_loop() # type: ignore[attr-defined] 77 | 78 | ACTIVATED_ASYNC_MODE = True 79 | -------------------------------------------------------------------------------- /questionary/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.1.0" 2 | -------------------------------------------------------------------------------- /scripts/validate_version.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | from typing import Optional 5 | from typing import Text 6 | 7 | import toml 8 | 9 | version_file_path = Path("questionary/version.py") 10 | 11 | pyproject_file_path = Path("pyproject.toml") 12 | 13 | 14 | def get_pyproject_version(): 15 | """Return the project version specified in the poetry build configuration.""" 16 | data = toml.load(pyproject_file_path) 17 | return data["tool"]["poetry"]["version"] 18 | 19 | 20 | def get_current_version() -> Text: 21 | """Return the current library version as specified in the code.""" 22 | 23 | if not version_file_path.is_file(): 24 | raise FileNotFoundError( 25 | f"Failed to find version file at {version_file_path().absolute()}" 26 | ) 27 | 28 | # context in which we evaluate the version py - 29 | # to be able to access the defined version, it already needs to live in the 30 | # context passed to exec 31 | _globals = {"__version__": ""} 32 | with open(version_file_path) as f: 33 | exec(f.read(), _globals) 34 | 35 | return _globals["__version__"] 36 | 37 | 38 | def get_tagged_version() -> Optional[Text]: 39 | """Return the version specified in a tagged git commit.""" 40 | return os.environ.get("TRAVIS_TAG") 41 | 42 | 43 | if __name__ == "__main__": 44 | if get_pyproject_version() != get_current_version(): 45 | print( 46 | f"Version in {pyproject_file_path} does not correspond " 47 | f"to the version in {version_file_path}! The version needs to be " 48 | f"set to the same value in both places." 49 | ) 50 | sys.exit(1) 51 | elif get_tagged_version() and get_tagged_version() != get_current_version(): 52 | print( 53 | f"Tagged version does not correspond to the version " 54 | f"in {version_file_path}!" 55 | ) 56 | sys.exit(1) 57 | elif get_tagged_version() and get_tagged_version() != get_pyproject_version(): 58 | print( 59 | f"Tagged version does not correspond to the version " 60 | f"in {pyproject_file_path}!" 61 | ) 62 | sys.exit(1) 63 | else: 64 | print("Versions look good!") 65 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/tests/conftest.py -------------------------------------------------------------------------------- /tests/prompts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmbo/questionary/584f9ae0e10869179e179b02a83d5383c5779ad0/tests/prompts/__init__.py -------------------------------------------------------------------------------- /tests/prompts/test_autocomplete.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from tests.utils import KeyInputs 6 | from tests.utils import feed_cli_with_input 7 | 8 | 9 | def test_autocomplete(): 10 | message = "Pick your poison " 11 | text = "python3\r" 12 | kwargs = { 13 | "choices": ["python3", "python2"], 14 | } 15 | result, cli = feed_cli_with_input("autocomplete", message, text, **kwargs) 16 | assert result == "python3" 17 | 18 | 19 | def test_no_choices_autocomplete(): 20 | message = "Pick your poison " 21 | text = "python2\r" 22 | 23 | with pytest.raises(ValueError): 24 | feed_cli_with_input("autocomplete", message, text, choices=[]) 25 | 26 | 27 | def test_validate_autocomplete(): 28 | message = "Pick your poison" 29 | text = "python123\r" 30 | 31 | kwargs = { 32 | "choices": ["python3", "python2", "python123"], 33 | } 34 | result, cli = feed_cli_with_input( 35 | "autocomplete", 36 | message, 37 | text, 38 | validate=lambda x: "c++" not in x or "?", 39 | **kwargs, 40 | ) 41 | assert result == "python123" 42 | 43 | 44 | def test_use_tab_autocomplete(): 45 | message = "Pick your poison" 46 | texts = ["p", KeyInputs.TAB + KeyInputs.TAB + KeyInputs.ENTER + "\r"] 47 | kwargs = { 48 | "choices": ["python3", "python2", "python123"], 49 | } 50 | result, cli = feed_cli_with_input("autocomplete", message, texts, **kwargs) 51 | assert result == "python2" 52 | 53 | 54 | def test_use_key_tab_autocomplete(): 55 | message = "Pick your poison" 56 | texts = [ 57 | "p", 58 | KeyInputs.TAB + KeyInputs.TAB + KeyInputs.TAB + KeyInputs.ENTER + "\r", 59 | ] 60 | kwargs = { 61 | "choices": ["python3", "python2", "python123", "c++"], 62 | } 63 | result, cli = feed_cli_with_input("autocomplete", message, texts, **kwargs) 64 | assert result == "python123" 65 | 66 | 67 | def test_ignore_case_autocomplete(): 68 | message = ("Pick your poison",) 69 | kwargs = { 70 | "choices": ["python3", "python2"], 71 | } 72 | texts = ["P", KeyInputs.TAB + KeyInputs.TAB + KeyInputs.ENTER + "\r"] 73 | result, cli = feed_cli_with_input( 74 | "autocomplete", message, texts, **kwargs, ignore_case=False 75 | ) 76 | assert result == "P" 77 | 78 | texts = ["p", KeyInputs.TAB + KeyInputs.TAB + KeyInputs.ENTER + "\r"] 79 | 80 | result, cli = feed_cli_with_input( 81 | "autocomplete", message, texts, **kwargs, ignore_case=True 82 | ) 83 | assert result == "python2" 84 | 85 | 86 | def test_match_middle_autocomplete(): 87 | message = ("Pick your poison",) 88 | kwargs = { 89 | "choices": ["python3", "python2"], 90 | } 91 | texts = ["t", KeyInputs.TAB + KeyInputs.TAB + KeyInputs.ENTER + "\r"] 92 | result, cli = feed_cli_with_input( 93 | "autocomplete", message, texts, **kwargs, match_middle=False 94 | ) 95 | assert result == "t" 96 | 97 | result, cli = feed_cli_with_input( 98 | "autocomplete", message, texts, **kwargs, match_middle=True 99 | ) 100 | assert result == "python2" 101 | -------------------------------------------------------------------------------- /tests/prompts/test_checkbox.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from questionary import Choice 5 | from questionary import Separator 6 | from tests.utils import KeyInputs 7 | from tests.utils import feed_cli_with_input 8 | 9 | 10 | def test_submit_empty(): 11 | message = "Foo message" 12 | kwargs = {"choices": ["foo", "bar", "bazz"]} 13 | text = KeyInputs.ENTER + "\r" 14 | 15 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 16 | assert result == [] 17 | 18 | 19 | def test_select_first_choice(): 20 | message = "Foo message" 21 | kwargs = {"choices": ["foo", "bar", "bazz"]} 22 | text = KeyInputs.SPACE + KeyInputs.ENTER + "\r" 23 | 24 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 25 | assert result == ["foo"] 26 | 27 | 28 | def test_select_with_instruction(): 29 | message = "Foo message" 30 | kwargs = {"choices": ["foo", "bar", "bazz"], "instruction": "sample instruction"} 31 | text = KeyInputs.SPACE + KeyInputs.ENTER + "\r" 32 | 33 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 34 | assert result == ["foo"] 35 | 36 | 37 | def test_select_first_choice_with_token_title(): 38 | message = "Foo message" 39 | kwargs = { 40 | "choices": [ 41 | Choice(title=[("class:text", "foo")]), 42 | Choice(title=[("class:text", "bar")]), 43 | Choice(title=[("class:text", "bazz")]), 44 | ] 45 | } 46 | text = KeyInputs.SPACE + KeyInputs.ENTER + "\r" 47 | 48 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 49 | assert result == ["foo"] 50 | 51 | 52 | def test_select_disabled_choices_if_they_are_default(): 53 | message = "Foo message" 54 | kwargs = { 55 | "choices": [ 56 | Choice("foo", checked=True), 57 | Choice("bar", disabled="unavailable", checked=True), 58 | Choice("baz", disabled="unavailable"), 59 | ] 60 | } 61 | text = KeyInputs.ENTER + "\r" 62 | 63 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 64 | assert result == ["foo", "bar"] 65 | 66 | 67 | def test_select_and_deselct(): 68 | message = "Foo message" 69 | kwargs = {"choices": ["foo", "bar", "bazz"]} 70 | text = ( 71 | KeyInputs.SPACE 72 | + KeyInputs.DOWN 73 | + KeyInputs.SPACE 74 | + KeyInputs.SPACE 75 | + KeyInputs.ENTER 76 | + "\r" 77 | ) 78 | 79 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 80 | assert result == ["foo"] 81 | 82 | 83 | def test_select_first_and_third_choice(): 84 | message = "Foo message" 85 | kwargs = {"choices": ["foo", "bar", "bazz"]} 86 | # DOWN and `j` should do the same 87 | text = ( 88 | KeyInputs.SPACE 89 | + KeyInputs.DOWN 90 | + "j" 91 | + KeyInputs.SPACE 92 | + KeyInputs.ENTER 93 | + "\r" 94 | ) 95 | 96 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 97 | assert result == ["foo", "bazz"] 98 | 99 | 100 | def test_select_first_and_third_choice_using_emacs_keys(): 101 | message = "Foo message" 102 | kwargs = {"choices": ["foo", "bar", "bazz"]} 103 | text = ( 104 | KeyInputs.SPACE 105 | + KeyInputs.CONTROLN 106 | + KeyInputs.CONTROLN 107 | + KeyInputs.SPACE 108 | + KeyInputs.ENTER 109 | + "\r" 110 | ) 111 | 112 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 113 | assert result == ["foo", "bazz"] 114 | 115 | 116 | def test_cycle_to_first_choice(): 117 | message = "Foo message" 118 | kwargs = {"choices": ["foo", "bar", "bazz"]} 119 | text = ( 120 | KeyInputs.DOWN 121 | + KeyInputs.DOWN 122 | + KeyInputs.DOWN 123 | + KeyInputs.SPACE 124 | + KeyInputs.ENTER 125 | + "\r" 126 | ) 127 | 128 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 129 | assert result == ["foo"] 130 | 131 | 132 | def test_cycle_backwards(): 133 | message = "Foo message" 134 | kwargs = {"choices": ["foo", "bar", "bazz"]} 135 | text = KeyInputs.UP + KeyInputs.SPACE + KeyInputs.ENTER + "\r" 136 | 137 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 138 | assert result == ["bazz"] 139 | 140 | 141 | def test_cycle_backwards_using_emacs_keys(): 142 | message = "Foo message" 143 | kwargs = {"choices": ["foo", "bar", "bazz"]} 144 | text = KeyInputs.CONTROLP + KeyInputs.SPACE + KeyInputs.ENTER + "\r" 145 | 146 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 147 | assert result == ["bazz"] 148 | 149 | 150 | def test_separator_down(): 151 | message = "Foo message" 152 | kwargs = {"choices": ["foo", Separator(), "bazz"]} 153 | text = KeyInputs.DOWN + KeyInputs.SPACE + KeyInputs.ENTER + "\r" 154 | 155 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 156 | assert result == ["bazz"] 157 | 158 | 159 | def test_separator_up(): 160 | message = "Foo message" 161 | kwargs = {"choices": ["foo", Separator(), "bazz", Separator("--END--")]} 162 | # UP and `k` should do the same 163 | text = KeyInputs.UP + "k" + KeyInputs.SPACE + KeyInputs.ENTER + "\r" 164 | 165 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 166 | assert result == ["foo"] 167 | 168 | 169 | def test_select_all(): 170 | message = "Foo message" 171 | kwargs = { 172 | "choices": [ 173 | {"name": "foo", "checked": True}, 174 | Separator(), 175 | {"name": "bar", "disabled": "nope"}, 176 | "bazz", 177 | Separator("--END--"), 178 | ] 179 | } 180 | text = KeyInputs.UP + "a" + KeyInputs.ENTER + "\r" 181 | 182 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 183 | assert result == ["foo", "bazz"] 184 | 185 | 186 | def test_select_all_deselect(): 187 | message = "Foo message" 188 | kwargs = { 189 | "choices": [ 190 | {"name": "foo", "checked": True}, 191 | Separator(), 192 | {"name": "bar", "disabled": "nope"}, 193 | "bazz", 194 | Separator("--END--"), 195 | ] 196 | } 197 | text = KeyInputs.UP + "a" + "a" + KeyInputs.ENTER + "\r" 198 | 199 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 200 | assert result == [] 201 | 202 | 203 | def test_select_invert(): 204 | message = "Foo message" 205 | kwargs = { 206 | "choices": [ 207 | {"name": "foo", "checked": True}, 208 | Separator(), 209 | {"name": "bar", "disabled": "nope"}, 210 | "bazz", 211 | Separator("--END--"), 212 | ] 213 | } 214 | text = KeyInputs.UP + "i" + KeyInputs.ENTER + "\r" 215 | 216 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 217 | assert result == ["bazz"] 218 | 219 | 220 | def test_list_random_input(): 221 | message = "Foo message" 222 | kwargs = {"choices": ["foo", "bazz"]} 223 | text = "sdf" + KeyInputs.ENTER + "\r" 224 | 225 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 226 | assert result == [] 227 | 228 | 229 | def test_list_ctr_c(): 230 | message = "Foo message" 231 | kwargs = {"choices": ["foo", "bazz"]} 232 | text = KeyInputs.CONTROLC 233 | 234 | with pytest.raises(KeyboardInterrupt): 235 | feed_cli_with_input("checkbox", message, text, **kwargs) 236 | 237 | 238 | def test_checkbox_initial_choice(): 239 | message = "Foo message" 240 | choice = Choice("bazz") 241 | kwargs = {"choices": ["foo", choice], "initial_choice": choice} 242 | text = KeyInputs.SPACE + KeyInputs.ENTER + "\r" 243 | 244 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 245 | assert result == ["bazz"] 246 | 247 | 248 | def test_select_initial_choice_string(): 249 | message = "Foo message" 250 | kwargs = {"choices": ["foo", "bazz"], "initial_choice": "bazz"} 251 | text = KeyInputs.SPACE + KeyInputs.ENTER + "\r" 252 | 253 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 254 | assert result == ["bazz"] 255 | 256 | 257 | def test_select_initial_choice_duplicate(): 258 | message = "Foo message" 259 | choice = Choice("foo") 260 | kwargs = {"choices": ["foo", choice, "bazz"], "initial_choice": choice} 261 | text = KeyInputs.DOWN + KeyInputs.SPACE + KeyInputs.ENTER + "\r" 262 | 263 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 264 | assert result == ["bazz"] 265 | 266 | 267 | def test_checkbox_initial_choice_not_selectable(): 268 | message = "Foo message" 269 | separator = Separator() 270 | kwargs = {"choices": ["foo", "bazz", separator], "initial_choice": separator} 271 | text = KeyInputs.ENTER + "\r" 272 | 273 | with pytest.raises(ValueError): 274 | feed_cli_with_input("checkbox", message, text, **kwargs) 275 | 276 | 277 | def test_checkbox_initial_choice_non_existant(): 278 | message = "Foo message" 279 | kwargs = {"choices": ["foo", "bazz"], "initial_choice": "bar"} 280 | text = KeyInputs.ENTER + "\r" 281 | 282 | with pytest.raises(ValueError): 283 | feed_cli_with_input("checkbox", message, text, **kwargs) 284 | 285 | 286 | def test_validate_default_message(): 287 | message = "Foo message" 288 | kwargs = {"choices": ["foo", "bar", "bazz"], "validate": lambda a: len(a) != 0} 289 | text = KeyInputs.ENTER + "i" + KeyInputs.ENTER + "\r" 290 | 291 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 292 | assert result == ["foo", "bar", "bazz"] 293 | 294 | 295 | def test_validate_with_message(): 296 | message = "Foo message" 297 | kwargs = { 298 | "choices": ["foo", "bar", "bazz"], 299 | "validate": lambda a: True if len(a) > 0 else "Error Message", 300 | } 301 | text = KeyInputs.ENTER + "i" + KeyInputs.ENTER + "\r" 302 | 303 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 304 | assert result == ["foo", "bar", "bazz"] 305 | 306 | 307 | def test_validate_not_callable(): 308 | message = "Foo message" 309 | kwargs = {"choices": ["foo", "bar", "bazz"], "validate": "invalid"} 310 | text = KeyInputs.ENTER + "i" + KeyInputs.ENTER + "\r" 311 | 312 | with pytest.raises(ValueError): 313 | feed_cli_with_input("checkbox", message, text, **kwargs) 314 | 315 | 316 | def test_proper_type_returned(): 317 | message = "Foo message" 318 | kwargs = { 319 | "choices": [ 320 | Choice("one", value=1), 321 | Choice("two", value="foo"), 322 | Choice("three", value=[3, "bar"]), 323 | ] 324 | } 325 | text = ( 326 | KeyInputs.SPACE 327 | + KeyInputs.DOWN 328 | + KeyInputs.SPACE 329 | + KeyInputs.DOWN 330 | + KeyInputs.SPACE 331 | + KeyInputs.DOWN 332 | + KeyInputs.ENTER 333 | + "\r" 334 | ) 335 | 336 | result, cli = feed_cli_with_input("checkbox", message, text, **kwargs) 337 | assert result == [1, "foo", [3, "bar"]] 338 | 339 | 340 | def test_fail_on_no_method_to_move_selection(): 341 | message = "Foo message" 342 | kwargs = { 343 | "choices": ["foo", Choice("bar", disabled="bad"), "bazz"], 344 | "use_shortcuts": False, 345 | "use_arrow_keys": False, 346 | "use_jk_keys": False, 347 | "use_emacs_keys": False, 348 | } 349 | text = KeyInputs.ENTER + "\r" 350 | 351 | with pytest.raises(ValueError): 352 | feed_cli_with_input("checkbox", message, text, **kwargs) 353 | 354 | 355 | def test_select_filter_first_choice(): 356 | message = "Foo message" 357 | kwargs = {"choices": ["foo", "bar", "bazz"]} 358 | text = KeyInputs.SPACE + KeyInputs.ENTER + "\r" 359 | 360 | result, cli = feed_cli_with_input( 361 | "checkbox", 362 | message, 363 | text, 364 | use_search_filter=True, 365 | use_jk_keys=False, 366 | **kwargs, 367 | ) 368 | assert result == ["foo"] 369 | 370 | 371 | def test_select_filter_multiple_after_search(): 372 | message = "Foo message" 373 | kwargs = {"choices": ["foo", "bar", "bazz", "buzz"]} 374 | text = ( 375 | KeyInputs.SPACE 376 | + "bu" 377 | + KeyInputs.SPACE 378 | + KeyInputs.BACK 379 | + KeyInputs.BACK 380 | + "\r" 381 | ) 382 | 383 | result, cli = feed_cli_with_input( 384 | "checkbox", 385 | message, 386 | text, 387 | use_search_filter=True, 388 | use_jk_keys=False, 389 | **kwargs, 390 | ) 391 | assert result == ["foo", "buzz"] 392 | -------------------------------------------------------------------------------- /tests/prompts/test_common.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest.mock import Mock 3 | from unittest.mock import call 4 | 5 | import pytest 6 | from prompt_toolkit.document import Document 7 | from prompt_toolkit.input.defaults import create_pipe_input 8 | from prompt_toolkit.output import DummyOutput 9 | from prompt_toolkit.styles import Attrs 10 | from prompt_toolkit.validation import ValidationError 11 | from prompt_toolkit.validation import Validator 12 | 13 | from questionary import Choice 14 | from questionary.prompts import common 15 | from questionary.prompts.common import InquirerControl 16 | from questionary.prompts.common import build_validator 17 | from questionary.prompts.common import print_formatted_text 18 | from tests.utils import prompt_toolkit_version 19 | 20 | 21 | def test_to_many_choices_for_shortcut_assignment(): 22 | ic = InquirerControl([str(i) for i in range(1, 100)], use_shortcuts=True) 23 | 24 | # IC should fail gracefully when running out of shortcuts 25 | assert len(list(filter(lambda x: x.shortcut_key is not None, ic.choices))) == len( 26 | InquirerControl.SHORTCUT_KEYS 27 | ) 28 | 29 | 30 | def test_validator_bool_function(): 31 | def validate(t): 32 | return len(t) == 3 33 | 34 | validator = build_validator(validate) 35 | assert validator.validate(Document("foo")) is None # should not raise 36 | 37 | 38 | def test_validator_bool_function_fails(): 39 | def validate(t): 40 | return len(t) == 3 41 | 42 | validator = build_validator(validate) 43 | with pytest.raises(ValidationError) as e: 44 | validator.validate(Document("fooooo")) 45 | 46 | assert e.value.message == "Invalid input" 47 | 48 | 49 | def test_validator_instance(): 50 | def validate(t): 51 | return len(t) == 3 52 | 53 | validator = Validator.from_callable(validate) 54 | 55 | validator = build_validator(validator) 56 | assert validator.validate(Document("foo")) is None # should not raise 57 | 58 | 59 | def test_validator_instance_fails(): 60 | def validate(t): 61 | return len(t) == 3 62 | 63 | validator = Validator.from_callable(validate, error_message="invalid input") 64 | with pytest.raises(ValidationError) as e: 65 | validator.validate(Document("fooooo")) 66 | 67 | assert e.value.message == "invalid input" 68 | 69 | 70 | def test_blank_line_fix(): 71 | def get_prompt_tokens(): 72 | return [("class:question", "What is your favourite letter?")] 73 | 74 | ic = InquirerControl(["a", "b", "c"]) 75 | 76 | async def run(inp): 77 | inp.send_text("") 78 | layout = common.create_inquirer_layout( 79 | ic, get_prompt_tokens, input=inp, output=DummyOutput() 80 | ) 81 | 82 | # usually this would be 2000000000000000000000000000000 83 | # but `common._fix_unecessary_blank_lines` makes sure 84 | # the main window is not as greedy (avoiding blank lines) 85 | assert ( 86 | layout.container.preferred_height(100, 200).max 87 | == 1000000000000000000000000000001 88 | ) 89 | 90 | if prompt_toolkit_version < (3, 0, 29): 91 | inp = create_pipe_input() 92 | try: 93 | return asyncio.run(run(inp)) 94 | finally: 95 | inp.close() 96 | else: 97 | with create_pipe_input() as inp: 98 | asyncio.run(run(inp)) 99 | 100 | 101 | def test_prompt_highlight_coexist(): 102 | ic = InquirerControl(["a", "b", "c"]) 103 | 104 | expected_tokens = [ 105 | ("class:pointer", " » "), 106 | ("[SetCursorPosition]", ""), 107 | ("class:text", "○ "), 108 | ("class:highlighted", "a"), 109 | ("", "\n"), 110 | ("class:text", " "), 111 | ("class:text", "○ "), 112 | ("class:text", "b"), 113 | ("", "\n"), 114 | ("class:text", " "), 115 | ("class:text", "○ "), 116 | ("class:text", "c"), 117 | ] 118 | assert ic.pointed_at == 0 119 | assert ic._get_choice_tokens() == expected_tokens 120 | 121 | ic.select_previous() 122 | expected_tokens = [ 123 | ("class:text", " "), 124 | ("class:text", "○ "), 125 | ("class:text", "a"), 126 | ("", "\n"), 127 | ("class:text", " "), 128 | ("class:text", "○ "), 129 | ("class:text", "b"), 130 | ("", "\n"), 131 | ("class:pointer", " » "), 132 | ("[SetCursorPosition]", ""), 133 | ("class:text", "○ "), 134 | ("class:highlighted", "c"), 135 | ] 136 | assert ic.pointed_at == 2 137 | assert ic._get_choice_tokens() == expected_tokens 138 | 139 | 140 | def test_prompt_show_answer_with_shortcuts(): 141 | ic = InquirerControl( 142 | ["a", Choice("b", shortcut_key=False), "c"], 143 | show_selected=True, 144 | use_shortcuts=True, 145 | ) 146 | 147 | expected_tokens = [ 148 | ("class:pointer", " » "), 149 | ("[SetCursorPosition]", ""), 150 | ("class:text", "○ "), 151 | ("class:highlighted", "1) a"), 152 | ("", "\n"), 153 | ("class:text", " "), 154 | ("class:text", "○ "), 155 | ("class:text", "-) b"), 156 | ("", "\n"), 157 | ("class:text", " "), 158 | ("class:text", "○ "), 159 | ("class:text", "2) c"), 160 | ("", "\n"), 161 | ("class:text", " Answer: 1) a"), 162 | ] 163 | assert ic.pointed_at == 0 164 | assert ic._get_choice_tokens() == expected_tokens 165 | 166 | ic.select_next() 167 | expected_tokens = [ 168 | ("class:text", " "), 169 | ("class:text", "○ "), 170 | ("class:text", "1) a"), 171 | ("", "\n"), 172 | ("class:pointer", " » "), 173 | ("[SetCursorPosition]", ""), 174 | ("class:text", "○ "), 175 | ("class:highlighted", "-) b"), 176 | ("", "\n"), 177 | ("class:text", " "), 178 | ("class:text", "○ "), 179 | ("class:text", "2) c"), 180 | ("", "\n"), 181 | ("class:text", " Answer: -) b"), 182 | ] 183 | assert ic.pointed_at == 1 184 | assert ic._get_choice_tokens() == expected_tokens 185 | 186 | 187 | def test_print(monkeypatch): 188 | mock = Mock(return_value=None) 189 | monkeypatch.setattr(DummyOutput, "write", mock) 190 | 191 | print_formatted_text("Hello World", output=DummyOutput()) 192 | 193 | mock.assert_has_calls([call("Hello World"), call("\r\n")]) 194 | 195 | 196 | def test_print_with_style(monkeypatch): 197 | mock = Mock(return_value=None) 198 | monkeypatch.setattr(DummyOutput, "write", mock.write) 199 | monkeypatch.setattr(DummyOutput, "set_attributes", mock.set_attributes) 200 | 201 | print_formatted_text( 202 | "Hello World", style="bold italic fg:darkred", output=DummyOutput() 203 | ) 204 | 205 | assert len(mock.method_calls) == 4 206 | assert mock.method_calls[0][0] == "set_attributes" 207 | 208 | if prompt_toolkit_version < (3, 0, 20): 209 | assert mock.method_calls[0][1][0] == Attrs( 210 | color="8b0000", 211 | bgcolor="", 212 | bold=True, 213 | underline=False, 214 | italic=True, 215 | blink=False, 216 | reverse=False, 217 | hidden=False, 218 | ) 219 | else: 220 | assert mock.method_calls[0][1][0] == Attrs( 221 | color="8b0000", 222 | bgcolor="", 223 | bold=True, 224 | underline=False, 225 | italic=True, 226 | blink=False, 227 | reverse=False, 228 | hidden=False, 229 | strike=False, 230 | ) 231 | 232 | assert mock.method_calls[1][0] == "write" 233 | assert mock.method_calls[1][1][0] == "Hello World" 234 | 235 | 236 | def test_prompt_show_description(): 237 | ic = InquirerControl( 238 | ["a", Choice("b", description="B")], 239 | show_selected=True, 240 | show_description=True, 241 | ) 242 | 243 | expected_tokens = [ 244 | ("class:pointer", " » "), 245 | ("[SetCursorPosition]", ""), 246 | ("class:text", "○ "), 247 | ("class:highlighted", "a"), 248 | ("", "\n"), 249 | ("class:text", " "), 250 | ("class:text", "○ "), 251 | ("class:text", "b"), 252 | ("", "\n"), 253 | ("class:text", " Answer: a"), 254 | ] 255 | assert ic.pointed_at == 0 256 | assert ic._get_choice_tokens() == expected_tokens 257 | 258 | ic.select_next() 259 | expected_tokens = [ 260 | ("class:text", " "), 261 | ("class:text", "○ "), 262 | ("class:text", "a"), 263 | ("", "\n"), 264 | ("class:pointer", " » "), 265 | ("[SetCursorPosition]", ""), 266 | ("class:text", "○ "), 267 | ("class:highlighted", "b"), 268 | ("", "\n"), 269 | ("class:text", " Answer: b"), 270 | ("class:text", " Description: B"), 271 | ] 272 | assert ic.pointed_at == 1 273 | assert ic._get_choice_tokens() == expected_tokens 274 | -------------------------------------------------------------------------------- /tests/prompts/test_confirm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from tests.utils import KeyInputs 5 | from tests.utils import feed_cli_with_input 6 | 7 | 8 | def test_confirm_enter_default_yes(): 9 | message = "Foo message" 10 | text = KeyInputs.ENTER + "\r" 11 | 12 | result, cli = feed_cli_with_input("confirm", message, text) 13 | assert result is True 14 | 15 | 16 | def test_confirm_enter_default_no(): 17 | message = "Foo message" 18 | text = KeyInputs.ENTER + "\r" 19 | 20 | result, cli = feed_cli_with_input("confirm", message, text, default=False) 21 | assert result is False 22 | 23 | 24 | def test_confirm_yes(): 25 | message = "Foo message" 26 | text = "y" + "\r" 27 | 28 | result, cli = feed_cli_with_input("confirm", message, text) 29 | assert result is True 30 | 31 | 32 | def test_confirm_no(): 33 | message = "Foo message" 34 | text = "n" + "\r" 35 | 36 | result, cli = feed_cli_with_input("confirm", message, text) 37 | assert result is False 38 | 39 | 40 | def test_confirm_big_yes(): 41 | message = "Foo message" 42 | text = "Y" + "\r" 43 | 44 | result, cli = feed_cli_with_input("confirm", message, text) 45 | assert result is True 46 | 47 | 48 | def test_confirm_big_no(): 49 | message = "Foo message" 50 | text = "N" + "\r" 51 | 52 | result, cli = feed_cli_with_input("confirm", message, text) 53 | assert result is False 54 | 55 | 56 | def test_confirm_random_input(): 57 | message = "Foo message" 58 | text = "my stuff" + KeyInputs.ENTER + "\r" 59 | 60 | result, cli = feed_cli_with_input("confirm", message, text) 61 | assert result is True 62 | 63 | 64 | def test_confirm_ctr_c(): 65 | message = "Foo message" 66 | text = KeyInputs.CONTROLC 67 | 68 | with pytest.raises(KeyboardInterrupt): 69 | feed_cli_with_input("confirm", message, text) 70 | 71 | 72 | def test_confirm_not_autoenter_yes(): 73 | message = "Foo message" 74 | text = "n" + "y" + KeyInputs.ENTER + "\r" 75 | 76 | result, cli = feed_cli_with_input("confirm", message, text, auto_enter=False) 77 | assert result is True 78 | 79 | 80 | def test_confirm_not_autoenter_no(): 81 | message = "Foo message" 82 | text = "n" + "y" + KeyInputs.ENTER + "\r" 83 | 84 | result, cli = feed_cli_with_input("confirm", message, text, auto_enter=False) 85 | assert result is True 86 | 87 | 88 | def test_confirm_not_autoenter_backspace(): 89 | message = "Foo message" 90 | text = "n" + KeyInputs.BACK + KeyInputs.ENTER + "\r" 91 | 92 | result, cli = feed_cli_with_input("confirm", message, text, auto_enter=False) 93 | assert result is True 94 | 95 | 96 | def test_confirm_instruction(): 97 | message = "Foo message" 98 | text = "Y" + "\r" 99 | 100 | result, cli = feed_cli_with_input( 101 | "confirm", message, text, instruction="Foo instruction" 102 | ) 103 | assert result is True 104 | -------------------------------------------------------------------------------- /tests/prompts/test_password.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from tests.utils import feed_cli_with_input 3 | 4 | 5 | def test_password_entry(): 6 | message = "What is your password" 7 | text = "my password\r" 8 | 9 | result, cli = feed_cli_with_input("password", message, text) 10 | assert result == "my password" 11 | -------------------------------------------------------------------------------- /tests/prompts/test_path.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import prompt_toolkit 3 | import pytest 4 | from prompt_toolkit.completion import Completer 5 | from prompt_toolkit.completion import Completion 6 | 7 | from tests.utils import KeyInputs 8 | from tests.utils import feed_cli_with_input 9 | 10 | 11 | @pytest.fixture 12 | def path_completion_tree(tmp_path): 13 | needed_directories = [ 14 | tmp_path / "foo", 15 | tmp_path / "foo" / "buz", # alphabetically after baz.any 16 | tmp_path / "bar", 17 | tmp_path / "baz", 18 | ] 19 | 20 | needed_files = [tmp_path / "foo" / "baz.any", tmp_path / "foo" / "foobar.any"] 21 | 22 | for d in needed_directories: 23 | d.mkdir() 24 | 25 | for f in needed_files: 26 | f.open("a").close() 27 | return tmp_path 28 | 29 | 30 | def test_path(): 31 | message = "Pick your path " 32 | text = "myfile.py" + KeyInputs.ENTER 33 | result, cli = feed_cli_with_input("path", message, text) 34 | assert result == "myfile.py" 35 | 36 | 37 | @pytest.mark.skipif( 38 | prompt_toolkit.__version__.startswith("2"), reason="requires prompt toolkit >= 3.0" 39 | ) 40 | def test_complete_path(path_completion_tree): 41 | test_input = str(path_completion_tree / "ba") 42 | message = "Pick your path" 43 | texts = [ 44 | test_input, 45 | KeyInputs.TAB + KeyInputs.TAB + KeyInputs.ENTER, 46 | KeyInputs.ENTER, 47 | ] 48 | 49 | result, cli = feed_cli_with_input("path", message, texts, 0.1) 50 | assert result == str(path_completion_tree / "baz") 51 | 52 | 53 | @pytest.mark.skipif( 54 | prompt_toolkit.__version__.startswith("2"), reason="requires prompt toolkit >= 3.0" 55 | ) 56 | def test_complete_requires_explicit_enter(path_completion_tree): 57 | # checks that an autocomplete needs to be confirmed with an enter and that the 58 | # enter doesn't directly submit the result 59 | test_input = str(path_completion_tree / "ba") 60 | message = "Pick your path" 61 | texts = [ 62 | test_input, 63 | KeyInputs.TAB + KeyInputs.TAB + KeyInputs.ENTER, 64 | "foo" + KeyInputs.ENTER, 65 | ] 66 | 67 | result, cli = feed_cli_with_input("path", message, texts, 0.1) 68 | 69 | assert result == str(path_completion_tree / "baz" / "foo") 70 | 71 | 72 | @pytest.mark.skipif( 73 | prompt_toolkit.__version__.startswith("2"), reason="requires prompt toolkit >= 3.0" 74 | ) 75 | def test_complete_path_directories_only(path_completion_tree): 76 | test_input = str(path_completion_tree / "foo" / "b") 77 | message = "Pick your path" 78 | texts = [test_input, KeyInputs.TAB + KeyInputs.ENTER, KeyInputs.ENTER] 79 | 80 | result, cli = feed_cli_with_input( 81 | "path", message, texts, 0.1, only_directories=True 82 | ) 83 | assert result == str(path_completion_tree / "foo" / "buz") 84 | 85 | 86 | @pytest.mark.skipif( 87 | prompt_toolkit.__version__.startswith("2"), reason="requires prompt toolkit >= 3.0" 88 | ) 89 | def test_get_paths(path_completion_tree): 90 | """Starting directories for path completion can be set.""" 91 | test_input = "ba" 92 | message = "Pick your path" 93 | texts = [ 94 | test_input, 95 | KeyInputs.TAB + KeyInputs.ENTER, 96 | KeyInputs.ENTER, 97 | ] 98 | 99 | result, cli = feed_cli_with_input( 100 | "path", 101 | message, 102 | texts, 103 | 0.1, 104 | get_paths=lambda: [str(path_completion_tree / "foo")], 105 | ) 106 | assert result == "baz.any" 107 | 108 | 109 | @pytest.mark.skipif( 110 | prompt_toolkit.__version__.startswith("2"), reason="requires prompt toolkit >= 3.0" 111 | ) 112 | def test_get_paths_validation(path_completion_tree): 113 | """`get_paths` must contain only existing directories.""" 114 | test_input = str(path_completion_tree / "ba") 115 | message = "Pick your path" 116 | texts = [ 117 | test_input, 118 | KeyInputs.TAB + KeyInputs.TAB + KeyInputs.ENTER, 119 | KeyInputs.ENTER, 120 | ] 121 | with pytest.raises(ValueError) as excinfo: 122 | feed_cli_with_input( 123 | "path", 124 | message, 125 | texts, 126 | 0.1, 127 | get_paths=lambda: [str(path_completion_tree / "not_existing")], 128 | ) 129 | assert "'get_paths' must return only existing directories" in str(excinfo) 130 | 131 | 132 | @pytest.mark.skipif( 133 | prompt_toolkit.__version__.startswith("2"), reason="requires prompt toolkit >= 3.0" 134 | ) 135 | def test_complete_custom_completer(): 136 | test_path = "foobar" 137 | 138 | class CustomCompleter(Completer): 139 | def get_completions(self, _, __): 140 | yield Completion(test_path) 141 | 142 | message = "Pick your path" 143 | texts = ["baz", KeyInputs.TAB + KeyInputs.ENTER, KeyInputs.ENTER] 144 | 145 | result, cli = feed_cli_with_input( 146 | "path", message, texts, 0.1, completer=CustomCompleter() 147 | ) 148 | assert result == "baz" + test_path 149 | -------------------------------------------------------------------------------- /tests/prompts/test_press_any_key_to_continue.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from tests.utils import feed_cli_with_input 4 | 5 | 6 | def test_press_any_key_to_continue_default_message(): 7 | message = None 8 | text = "c" 9 | result, cli = feed_cli_with_input("press_any_key_to_continue", message, text) 10 | 11 | assert result is None 12 | -------------------------------------------------------------------------------- /tests/prompts/test_rawselect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import uuid 3 | 4 | import pytest 5 | 6 | from questionary import Separator 7 | from tests.utils import KeyInputs 8 | from tests.utils import feed_cli_with_input 9 | 10 | 11 | def test_legacy_name(): 12 | message = "Foo message" 13 | kwargs = {"choices": ["foo", "bar", "bazz"]} 14 | text = "1" + KeyInputs.ENTER + "\r" 15 | 16 | result, cli = feed_cli_with_input("rawlist", message, text, **kwargs) 17 | assert result == "foo" 18 | 19 | 20 | def test_select_first_choice(): 21 | message = "Foo message" 22 | kwargs = {"choices": ["foo", "bar", "bazz"]} 23 | text = "1" + KeyInputs.ENTER + "\r" 24 | 25 | result, cli = feed_cli_with_input("rawselect", message, text, **kwargs) 26 | assert result == "foo" 27 | 28 | 29 | def test_select_second_choice(): 30 | message = "Foo message" 31 | kwargs = {"choices": ["foo", "bar", "bazz"]} 32 | text = "2" + KeyInputs.ENTER + "\r" 33 | 34 | result, cli = feed_cli_with_input("rawselect", message, text, **kwargs) 35 | assert result == "bar" 36 | 37 | 38 | def test_select_third_choice(): 39 | message = "Foo message" 40 | kwargs = {"choices": ["foo", "bar", "bazz"]} 41 | text = "2" + "3" + KeyInputs.ENTER + "\r" 42 | 43 | result, cli = feed_cli_with_input("rawselect", message, text, **kwargs) 44 | assert result == "bazz" 45 | 46 | 47 | def test_separator_shortcuts(): 48 | message = "Foo message" 49 | kwargs = {"choices": ["foo", Separator(), "bazz"]} 50 | text = "2" + KeyInputs.ENTER + "\r" 51 | 52 | result, cli = feed_cli_with_input("rawselect", message, text, **kwargs) 53 | assert result == "bazz" 54 | 55 | 56 | def test_duplicated_shortcuts(): 57 | message = "Foo message" 58 | kwargs = { 59 | "choices": [ 60 | {"name": "foo", "key": 1}, 61 | Separator(), 62 | {"name": "bar", "key": 1}, 63 | "bazz", 64 | Separator("--END--"), 65 | ] 66 | } 67 | text = "1" + KeyInputs.ENTER + "\r" 68 | 69 | with pytest.raises(ValueError): 70 | feed_cli_with_input("rawselect", message, text, **kwargs) 71 | 72 | 73 | def test_invalid_shortcuts(): 74 | message = "Foo message" 75 | kwargs = { 76 | "choices": [ 77 | {"name": "foo", "key": "asd"}, 78 | Separator(), 79 | {"name": "bar", "key": "1"}, 80 | "bazz", 81 | Separator("--END--"), 82 | ] 83 | } 84 | text = "1" + KeyInputs.ENTER + "\r" 85 | 86 | with pytest.raises(ValueError): 87 | feed_cli_with_input("rawselect", message, text, **kwargs) 88 | 89 | 90 | def test_to_many_choices(): 91 | message = "Foo message" 92 | kwargs = {"choices": [uuid.uuid4().hex for _ in range(0, 37)]} 93 | text = "1" + KeyInputs.ENTER + "\r" 94 | 95 | with pytest.raises(ValueError): 96 | feed_cli_with_input("rawselect", message, text, **kwargs) 97 | 98 | 99 | def test_select_random_input(): 100 | message = "Foo message" 101 | kwargs = {"choices": ["foo", "bazz"]} 102 | text = "2" + "some random input" + KeyInputs.ENTER + "\r" 103 | 104 | result, cli = feed_cli_with_input("rawselect", message, text, **kwargs) 105 | assert result == "bazz" 106 | 107 | 108 | def test_select_ctr_c(): 109 | message = "Foo message" 110 | kwargs = {"choices": ["foo", "bazz"]} 111 | text = KeyInputs.CONTROLC 112 | 113 | with pytest.raises(KeyboardInterrupt): 114 | feed_cli_with_input("rawselect", message, text, **kwargs) 115 | -------------------------------------------------------------------------------- /tests/prompts/test_text.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | from prompt_toolkit.validation import ValidationError 5 | from prompt_toolkit.validation import Validator 6 | 7 | from tests.utils import feed_cli_with_input 8 | 9 | 10 | def test_legacy_name(): 11 | message = "What is your name" 12 | text = "bob\r" 13 | 14 | result, cli = feed_cli_with_input("input", message, text) 15 | assert result == "bob" 16 | 17 | 18 | def test_text(): 19 | message = "What is your name" 20 | text = "bob\r" 21 | 22 | result, cli = feed_cli_with_input("text", message, text) 23 | assert result == "bob" 24 | 25 | 26 | def test_text_validate(): 27 | message = "What is your name" 28 | text = "Doe\r" 29 | 30 | result, cli = feed_cli_with_input( 31 | "text", 32 | message, 33 | text, 34 | validate=lambda val: val == "Doe" or "is your last name Doe?", 35 | ) 36 | assert result == "Doe" 37 | 38 | 39 | def test_text_validate_with_class(): 40 | class SimpleValidator(Validator): 41 | def validate(self, document): 42 | ok = re.match("[01][01][01]", document.text) 43 | if not ok: 44 | raise ValidationError( 45 | message="Binary FTW", cursor_position=len(document.text) 46 | ) 47 | 48 | message = "What is your name" 49 | text = "001\r" 50 | 51 | result, cli = feed_cli_with_input("text", message, text, validate=SimpleValidator) 52 | assert result == "001" 53 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit.output import DummyOutput 2 | 3 | from tests.utils import KeyInputs 4 | from tests.utils import execute_with_input_pipe 5 | 6 | 7 | def ask_with_patched_input(q, text): 8 | def run(inp): 9 | inp.send_text(text) 10 | return q(input=inp, output=DummyOutput()) 11 | 12 | return execute_with_input_pipe(run) 13 | 14 | 15 | def test_confirm_example(): 16 | from examples.confirm_continue import ask_dictstyle 17 | from examples.confirm_continue import ask_pystyle 18 | 19 | text = "n" + KeyInputs.ENTER + "\r" 20 | 21 | result_dict = ask_with_patched_input(ask_dictstyle, text) 22 | result_py = ask_with_patched_input(ask_pystyle, text) 23 | 24 | assert result_dict == {"continue": False} 25 | assert result_dict["continue"] == result_py 26 | 27 | 28 | def test_text_example(): 29 | from examples.text_phone_number import ask_dictstyle 30 | from examples.text_phone_number import ask_pystyle 31 | 32 | text = "1234567890" + KeyInputs.ENTER + "\r" 33 | 34 | result_dict = ask_with_patched_input(ask_dictstyle, text) 35 | result_py = ask_with_patched_input(ask_pystyle, text) 36 | 37 | assert result_dict == {"phone": "1234567890"} 38 | assert result_dict["phone"] == result_py 39 | 40 | 41 | def test_select_example(): 42 | from examples.select_restaurant import ask_dictstyle 43 | from examples.select_restaurant import ask_pystyle 44 | 45 | text = KeyInputs.DOWN + KeyInputs.ENTER + KeyInputs.ENTER + "\r" 46 | 47 | result_dict = ask_with_patched_input(ask_dictstyle, text) 48 | result_py = ask_with_patched_input(ask_pystyle, text) 49 | 50 | assert result_dict == {"theme": "Make a reservation"} 51 | assert result_dict["theme"] == result_py 52 | 53 | 54 | def test_rawselect_example(): 55 | from examples.rawselect_separator import ask_dictstyle 56 | from examples.rawselect_separator import ask_pystyle 57 | 58 | text = "3" + KeyInputs.ENTER + KeyInputs.ENTER + "\r" 59 | 60 | result_dict = ask_with_patched_input(ask_dictstyle, text) 61 | result_py = ask_with_patched_input(ask_pystyle, text) 62 | 63 | assert result_dict == {"theme": "Ask opening hours"} 64 | assert result_dict["theme"] == result_py 65 | 66 | 67 | def test_checkbox_example(): 68 | from examples.checkbox_separators import ask_dictstyle 69 | from examples.checkbox_separators import ask_pystyle 70 | 71 | text = "n" + KeyInputs.ENTER + KeyInputs.ENTER + KeyInputs.ENTER + "\r" 72 | 73 | result_dict = ask_with_patched_input(ask_dictstyle, text) 74 | result_py = ask_with_patched_input(ask_pystyle, text) 75 | 76 | assert result_dict == {"toppings": ["foo"]} 77 | assert result_dict["toppings"] == result_py 78 | 79 | 80 | def test_password_example(): 81 | from examples.password_git import ask_dictstyle 82 | from examples.password_git import ask_pystyle 83 | 84 | text = "asdf" + KeyInputs.ENTER + "\r" 85 | 86 | result_dict = ask_with_patched_input(ask_dictstyle, text) 87 | result_py = ask_with_patched_input(ask_pystyle, text) 88 | 89 | assert result_dict == {"password": "asdf"} 90 | assert result_dict["password"] == result_py 91 | 92 | 93 | def test_autocomplete_example(): 94 | from examples.autocomplete_ants import ask_dictstyle 95 | from examples.autocomplete_ants import ask_pystyle 96 | 97 | text = "Polyergus lucidus" + KeyInputs.ENTER + "\r" 98 | 99 | result_dict = ask_with_patched_input(ask_dictstyle, text) 100 | result_py = ask_with_patched_input(ask_pystyle, text) 101 | 102 | assert result_dict == {"ants": "Polyergus lucidus"} 103 | assert result_py == "Polyergus lucidus" 104 | 105 | 106 | def test_advanced_workflow_example(): 107 | from examples.advanced_workflow import ask_dictstyle 108 | 109 | text = ( 110 | KeyInputs.ENTER 111 | + "questionary" 112 | + KeyInputs.ENTER 113 | + KeyInputs.DOWN 114 | + KeyInputs.DOWN 115 | + KeyInputs.ENTER 116 | + "Hello World" 117 | + KeyInputs.ENTER 118 | + "\r" 119 | ) 120 | 121 | result_dict = ask_with_patched_input(ask_dictstyle, text) 122 | 123 | assert result_dict == { 124 | "intro": None, 125 | "conditional_step": True, 126 | "next_question": "questionary", 127 | "second_question": "Hello World", 128 | } 129 | -------------------------------------------------------------------------------- /tests/test_form.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit.output import DummyOutput 2 | from pytest import fail 3 | 4 | import questionary 5 | from questionary import form 6 | from tests.utils import KeyInputs 7 | from tests.utils import execute_with_input_pipe 8 | 9 | 10 | def example_form(inp): 11 | return form( 12 | q1=questionary.confirm("Hello?", input=inp, output=DummyOutput()), 13 | q2=questionary.select( 14 | "World?", choices=["foo", "bar"], input=inp, output=DummyOutput() 15 | ), 16 | ) 17 | 18 | 19 | def example_form_with_skip(inp): 20 | return form( 21 | q1=questionary.confirm("Hello?", input=inp, output=DummyOutput()), 22 | q2=questionary.select( 23 | "World?", choices=["foo", "bar"], input=inp, output=DummyOutput() 24 | ).skip_if(True, 42), 25 | ) 26 | 27 | 28 | def test_form_creation(): 29 | text = "Y" + KeyInputs.ENTER + "\r" 30 | 31 | def run(inp): 32 | inp.send_text(text) 33 | f = example_form(inp) 34 | result = f.unsafe_ask() 35 | assert result == {"q1": True, "q2": "foo"} 36 | 37 | execute_with_input_pipe(run) 38 | 39 | 40 | def test_form_skips_questions(): 41 | text = "Y" + KeyInputs.ENTER + "\r" 42 | 43 | def run(inp): 44 | inp.send_text(text) 45 | f = example_form_with_skip(inp) 46 | 47 | result = f.ask() 48 | 49 | assert result == {"q1": True, "q2": 42} 50 | 51 | execute_with_input_pipe(run) 52 | 53 | 54 | def test_form_skips_questions_unsafe_ask(): 55 | text = "Y" + KeyInputs.ENTER + "\r" 56 | 57 | def run(inp): 58 | inp.send_text(text) 59 | f = example_form_with_skip(inp) 60 | 61 | result = f.unsafe_ask() 62 | 63 | assert result == {"q1": True, "q2": 42} 64 | 65 | execute_with_input_pipe(run) 66 | 67 | 68 | def test_ask_should_catch_keyboard_exception(): 69 | def run(inp): 70 | try: 71 | inp.send_text(KeyInputs.CONTROLC) 72 | f = example_form(inp) 73 | 74 | result = f.ask() 75 | assert result == {} 76 | except KeyboardInterrupt: 77 | fail("Keyboard Interrupt should be caught by `ask()`") 78 | 79 | execute_with_input_pipe(run) 80 | -------------------------------------------------------------------------------- /tests/test_prompt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from questionary.prompt import PromptParameterException 4 | from questionary.prompt import prompt 5 | from tests.utils import patched_prompt 6 | 7 | 8 | def test_missing_message(): 9 | with pytest.raises(PromptParameterException): 10 | prompt([{"type": "confirm", "name": "continue", "default": True}]) 11 | 12 | 13 | def test_missing_type(): 14 | with pytest.raises(PromptParameterException): 15 | prompt( 16 | [ 17 | { 18 | "message": "Do you want to continue?", 19 | "name": "continue", 20 | "default": True, 21 | } 22 | ] 23 | ) 24 | 25 | 26 | def test_missing_name(): 27 | with pytest.raises(PromptParameterException): 28 | prompt( 29 | [ 30 | { 31 | "type": "confirm", 32 | "message": "Do you want to continue?", 33 | "default": True, 34 | } 35 | ] 36 | ) 37 | 38 | 39 | def test_invalid_question_type(): 40 | with pytest.raises(ValueError): 41 | prompt( 42 | [ 43 | { 44 | "type": "mytype", 45 | "message": "Do you want to continue?", 46 | "name": "continue", 47 | "default": True, 48 | } 49 | ] 50 | ) 51 | 52 | 53 | def test_missing_print_message(): 54 | """Test 'print' raises exception if missing 'message'""" 55 | with pytest.raises(PromptParameterException): 56 | prompt( 57 | [ 58 | { 59 | "name": "test", 60 | "type": "print", 61 | } 62 | ] 63 | ) 64 | 65 | 66 | def test_print_no_name(): 67 | """'print' type doesn't require a name so it 68 | should not throw PromptParameterException""" 69 | questions = [{"type": "print", "message": "Hello World"}] 70 | result = patched_prompt(questions, "") 71 | assert result == {} 72 | 73 | 74 | def test_print_with_name(): 75 | """'print' type should return {name: None} when name is provided""" 76 | questions = [{"name": "hello", "type": "print", "message": "Hello World"}] 77 | result = patched_prompt(questions, "") 78 | assert result == {"hello": None} 79 | -------------------------------------------------------------------------------- /tests/test_question.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import platform 3 | 4 | import pytest 5 | from prompt_toolkit.output import DummyOutput 6 | from pytest import fail 7 | 8 | from questionary import text 9 | from questionary.utils import is_prompt_toolkit_3 10 | from tests.utils import KeyInputs 11 | from tests.utils import execute_with_input_pipe 12 | 13 | 14 | def test_ask_should_catch_keyboard_exception(): 15 | def run(inp): 16 | inp.send_text(KeyInputs.CONTROLC) 17 | question = text("Hello?", input=inp, output=DummyOutput()) 18 | try: 19 | result = question.ask() 20 | assert result is None 21 | except KeyboardInterrupt: 22 | fail("Keyboard Interrupt should be caught by `ask()`") 23 | 24 | execute_with_input_pipe(run) 25 | 26 | 27 | def test_skipping_of_questions(): 28 | def run(inp): 29 | question = text("Hello?", input=inp, output=DummyOutput()).skip_if( 30 | condition=True, default=42 31 | ) 32 | response = question.ask() 33 | assert response == 42 34 | 35 | execute_with_input_pipe(run) 36 | 37 | 38 | def test_skipping_of_questions_unsafe(): 39 | def run(inp): 40 | question = text("Hello?", input=inp, output=DummyOutput()).skip_if( 41 | condition=True, default=42 42 | ) 43 | response = question.unsafe_ask() 44 | assert response == 42 45 | 46 | execute_with_input_pipe(run) 47 | 48 | 49 | def test_skipping_of_skipping_of_questions(): 50 | def run(inp): 51 | inp.send_text("World" + KeyInputs.ENTER + "\r") 52 | question = text("Hello?", input=inp, output=DummyOutput()).skip_if( 53 | condition=False, default=42 54 | ) 55 | response = question.ask() 56 | assert response == "World" and not response == 42 57 | 58 | execute_with_input_pipe(run) 59 | 60 | 61 | def test_skipping_of_skipping_of_questions_unsafe(): 62 | def run(inp): 63 | inp.send_text("World" + KeyInputs.ENTER + "\r") 64 | question = text("Hello?", input=inp, output=DummyOutput()).skip_if( 65 | condition=False, default=42 66 | ) 67 | response = question.unsafe_ask() 68 | assert response == "World" and not response == 42 69 | 70 | execute_with_input_pipe(run) 71 | 72 | 73 | @pytest.mark.skipif( 74 | not is_prompt_toolkit_3() and platform.system() == "Windows", 75 | reason="requires prompt_toolkit >= 3", 76 | ) 77 | def test_async_ask_question(): 78 | loop = asyncio.new_event_loop() 79 | 80 | def run(inp): 81 | inp.send_text("World" + KeyInputs.ENTER + "\r") 82 | question = text("Hello?", input=inp, output=DummyOutput()) 83 | response = loop.run_until_complete(question.ask_async()) 84 | assert response == "World" 85 | 86 | execute_with_input_pipe(run) 87 | 88 | 89 | def test_multiline_text(): 90 | def run(inp): 91 | inp.send_text(f"Hello{KeyInputs.ENTER}world{KeyInputs.ESCAPE}{KeyInputs.ENTER}") 92 | question = text("Hello?", input=inp, output=DummyOutput(), multiline=True) 93 | response = question.ask() 94 | assert response == "Hello\nworld" 95 | 96 | execute_with_input_pipe(run) 97 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from questionary import utils 2 | 3 | 4 | def test_default_values_of(): 5 | def f(a, b=2, c=None, *args, **kwargs): 6 | pass 7 | 8 | defaults = utils.default_values_of(f) 9 | assert defaults == ["b", "c", "args", "kwargs"] 10 | 11 | 12 | def test_default_values_of_no_args(): 13 | def f(): 14 | pass 15 | 16 | defaults = utils.default_values_of(f) 17 | assert defaults == [] 18 | 19 | 20 | def test_arguments_of(): 21 | def f(a, b=2, c=None, *args, **kwargs): 22 | pass 23 | 24 | defaults = utils.arguments_of(f) 25 | assert defaults == ["a", "b", "c", "args", "kwargs"] 26 | 27 | 28 | def test_arguments_of_no_args(): 29 | def f(): 30 | pass 31 | 32 | defaults = utils.arguments_of(f) 33 | assert defaults == [] 34 | 35 | 36 | def test_filter_kwargs(): 37 | def f(a, b=1, *, c=2): 38 | pass 39 | 40 | kwargs = { 41 | "a": 1, 42 | "b": 2, 43 | "c": 3, 44 | "d": 4, 45 | } 46 | 47 | filtered = utils.used_kwargs(kwargs, f) 48 | assert "a" in filtered 49 | assert "b" in filtered 50 | assert "c" in filtered 51 | assert "d" not in filtered 52 | 53 | 54 | def test_filter_kwargs_empty(): 55 | def f(): 56 | pass 57 | 58 | kwargs = { 59 | "a": 1, 60 | "b": 2, 61 | } 62 | 63 | filtered = utils.used_kwargs(kwargs, f) 64 | assert filtered == {} 65 | 66 | 67 | def test_required_arguments_of(): 68 | def f(a, b=2, c=None, *args, **kwargs): 69 | pass 70 | 71 | defaults = utils.required_arguments(f) 72 | assert defaults == ["a"] 73 | 74 | 75 | def test_required_arguments_of_no_args(): 76 | def f(): 77 | pass 78 | 79 | defaults = utils.required_arguments(f) 80 | assert defaults == [] 81 | 82 | 83 | def test_missing_arguments(): 84 | def f(a, b=2, c=None, *args, **kwargs): 85 | pass 86 | 87 | assert utils.missing_arguments(f, {}) == {"a"} 88 | assert utils.missing_arguments(f, {"a": 1}) == set() 89 | assert utils.missing_arguments(f, {"a": 1, "b": 2}) == set() 90 | 91 | 92 | def test_missing_arguments_of_no_args(): 93 | def f(): 94 | pass 95 | 96 | defaults = utils.missing_arguments(f, {}) 97 | assert defaults == set() 98 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | 4 | import prompt_toolkit 5 | from prompt_toolkit.input.defaults import create_pipe_input 6 | from prompt_toolkit.output import DummyOutput 7 | 8 | from questionary import prompt 9 | from questionary.prompts import prompt_by_name 10 | from questionary.utils import is_prompt_toolkit_3 11 | 12 | prompt_toolkit_version = tuple([int(v) for v in prompt_toolkit.VERSION]) 13 | 14 | 15 | class KeyInputs: 16 | DOWN = "\x1b[B" 17 | UP = "\x1b[A" 18 | LEFT = "\x1b[D" 19 | RIGHT = "\x1b[C" 20 | ENTER = "\r" 21 | ESCAPE = "\x1b" 22 | CONTROLC = "\x03" 23 | CONTROLN = "\x0e" 24 | CONTROLP = "\x10" 25 | BACK = "\x7f" 26 | SPACE = " " 27 | TAB = "\x09" 28 | ONE = "1" 29 | TWO = "2" 30 | THREE = "3" 31 | 32 | 33 | def feed_cli_with_input(_type, message, texts, sleep_time=1, **kwargs): 34 | """ 35 | Create a Prompt, feed it with the given user input and return the CLI 36 | object. 37 | 38 | You an provide multiple texts, the feeder will async sleep for `sleep_time` 39 | 40 | This returns a (result, Application) tuple. 41 | """ 42 | 43 | if not isinstance(texts, list): 44 | texts = [texts] 45 | 46 | def _create_input(inp): 47 | prompter = prompt_by_name(_type) 48 | application = prompter(message, input=inp, output=DummyOutput(), **kwargs) 49 | if is_prompt_toolkit_3(): 50 | loop = asyncio.new_event_loop() 51 | future_result = loop.create_task(application.unsafe_ask_async()) 52 | 53 | for i, text in enumerate(texts): 54 | # noinspection PyUnresolvedReferences 55 | inp.send_text(text) 56 | 57 | if i != len(texts) - 1: 58 | loop.run_until_complete(asyncio.sleep(sleep_time)) 59 | result = loop.run_until_complete(future_result) 60 | else: 61 | for text in texts: 62 | inp.send_text(text) 63 | result = application.unsafe_ask() 64 | return result, application 65 | 66 | return execute_with_input_pipe(_create_input) 67 | 68 | 69 | def patched_prompt(questions, text, **kwargs): 70 | """Create a prompt where the input and output are predefined.""" 71 | 72 | def run(inp): 73 | # noinspection PyUnresolvedReferences 74 | inp.send_text(text) 75 | result = prompt(questions, input=inp, output=DummyOutput(), **kwargs) 76 | return result 77 | 78 | return execute_with_input_pipe(run) 79 | 80 | 81 | def execute_with_input_pipe(func): 82 | if prompt_toolkit_version < (3, 0, 29): 83 | inp = create_pipe_input() 84 | try: 85 | return func(inp) 86 | finally: 87 | inp.close() 88 | else: 89 | with create_pipe_input() as inp: 90 | return func(inp) 91 | --------------------------------------------------------------------------------