├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ ├── fix_request.md
│ └── security-vulnerability.md
└── workflows
│ ├── lint.yml
│ ├── python-publish.yml
│ └── tests.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── .run
├── Make vhs.run.xml
├── Sphinx Build.run.xml
├── Sphinx Coverage .run.xml
├── pytest in tests.run.xml
└── run example.run.xml
├── CONTRIBUTING.md
├── ItsPrompt
├── data
│ ├── __init__.py
│ ├── checkbox.py
│ ├── expand.py
│ ├── select.py
│ ├── style.py
│ └── table.py
├── keyboard_handler.py
├── objects
│ ├── prompts
│ │ ├── option.py
│ │ ├── options_with_separator.py
│ │ ├── separator.py
│ │ └── type.py
│ └── table
│ │ ├── table_base.py
│ │ ├── table_df.py
│ │ ├── table_dict.py
│ │ └── table_list.py
├── prompt.py
├── prompts
│ ├── __init__.py
│ ├── checkbox.py
│ ├── confirm.py
│ ├── expand.py
│ ├── input.py
│ ├── raw_select.py
│ ├── select.py
│ └── table.py
└── py.typed
├── LICENSE
├── README.md
├── SECURITY.md
├── docs
├── Makefile
├── make.bat
├── requirements.txt
├── scripts
│ └── vhs
│ │ ├── checkbox.tape
│ │ ├── configuration
│ │ ├── base_config.tape
│ │ ├── big_window_config.tape
│ │ ├── config.sh
│ │ ├── gradient.png
│ │ └── small_window_config.tape
│ │ ├── confirm.tape
│ │ ├── demo.tape
│ │ ├── expand.tape
│ │ ├── generate.sh
│ │ ├── input.tape
│ │ ├── raw_select.tape
│ │ ├── select.tape
│ │ ├── table.tape
│ │ └── tape.template
└── source
│ ├── api
│ ├── objects.rst
│ └── prompt.rst
│ ├── conf.py
│ ├── development_guide
│ ├── documentation.rst
│ └── getting_started.rst
│ ├── guide
│ ├── getting_started.rst
│ ├── options_and_data.rst
│ ├── prompt_types.rst
│ ├── styling.rst
│ └── usage.rst
│ ├── index.rst
│ └── media
│ ├── ItsPrompt.gif
│ ├── checkbox.png
│ ├── confirm.png
│ ├── expand.png
│ ├── input.png
│ ├── raw_select.png
│ ├── select.png
│ ├── styling
│ ├── error.png
│ ├── grayout.png
│ ├── option.png
│ ├── question.png
│ ├── question_mark.png
│ ├── selected_option.png
│ ├── separator.png
│ ├── text.png
│ └── tooltip.png
│ ├── styling_input.png
│ ├── styling_input_annotated.png
│ ├── styling_raw_select.png
│ ├── styling_raw_select_annotated.png
│ └── table.png
├── example.py
├── examples
├── demo.py
└── demos
│ ├── checkbox_demo.py
│ ├── confirm_demo.py
│ ├── expand_demo.py
│ ├── input_demo.py
│ ├── raw_select_demo.py
│ ├── select_demo.py
│ └── table_demo.py
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
└── tests
├── __init__.py
├── conftest.py
├── data
├── test_checkbox.py
├── test_expand.py
├── test_select.py
├── test_style.py
└── test_table.py
└── prompts
├── __init__.py
├── test_checkbox_prompt.py
├── test_confirm_prompt.py
├── test_expand_prompt.py
├── test_input_prompt.py
├── test_raw_select_prompt.py
├── test_select_prompt.py
└── test_table_prompt.py
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]"
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 |
16 | 1. Go to '...'
17 | 2. Click on '....'
18 | 3. Scroll down to '....'
19 | 4. See error
20 |
21 | **Expected behavior**
22 | A clear and concise description of what you expected to happen.
23 |
24 | **Screenshots**
25 | If applicable, add screenshots to help explain your problem.
26 |
27 | **Device (please complete the following information):**
28 |
29 | - OS: [e.g. iOS]
30 | - Python [e.g. version, installed packages]
31 | - Version [e.g. 22]
32 |
33 | **Additional context**
34 | Add any other context about the problem here.
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[FEATURE]"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Feature description**
11 | Describe what you want to be added.
12 |
13 | **Additional Information**
14 | Write some information, e.g. what we need to add your feature or how you think this should be done.
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/fix_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Fix request
3 | about: Point out a fix that needs to be addressed
4 | title: "[FIX]"
5 | labels: fix
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Fix description**
11 | Describe what you want to be added.
12 |
13 | **Additional Information**
14 | Write some information, e.g. what we need to fix that problem or how you think this should be done.
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/security-vulnerability.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Security Vulnerability
3 | about: Show us a security vulnerability you have found and which needs to be addressed
4 | title: "[SECURITY] "
5 | labels: security
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the security concern**
11 | A clear and concise description of what security concern you encountered and why it is so bad.
12 |
13 | **To Reproduce**
14 | *If it's a general security concern, leave this section empty*
15 | Steps to reproduce the behavior:
16 |
17 | 1. Go to '...'
18 | 2. Click on '....'
19 | 3. Scroll down to '....'
20 | 4. See error
21 |
22 | **Screenshots**
23 | *If it's a general security concern, leave this section empty*
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Device (please complete the following information):**
27 | *If it's a general security concern, leave this section empty*
28 |
29 | - OS: [e.g. iOS]
30 | - Python [e.g. version, installed packages]
31 | - Version [e.g. 22]
32 |
33 | **Additional context**
34 | Add any other context about the problem here.
35 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: linting
2 | on:
3 | - push
4 | - pull_request
5 |
6 | jobs:
7 | typing-check:
8 | name: mypy Typing Check
9 | runs-on: ubuntu-latest
10 | steps:
11 | # setup python
12 | - name: Checkout
13 | uses: actions/checkout@v2
14 | - name: Setup python
15 | uses: actions/setup-python@v4
16 | with:
17 | python-version: "3.x"
18 | cache: "pip"
19 | cache-dependency-path: "requirements*.txt"
20 | - name: Install modules
21 | run: pip install -r requirements.txt -r requirements-dev.txt
22 |
23 | # run mypy
24 | - name: Run mypy
25 | run: python -m mypy
26 | formatting-check:
27 | name: YAPF Formatting Check
28 | runs-on: ubuntu-latest
29 | steps:
30 | - uses: actions/checkout@v3
31 | - name: run YAPF to test if python code is correctly formatted
32 | uses: AlexanderMelde/yapf-action@v1.0
33 | with:
34 | args: --verbose
35 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Upload Python Package
10 |
11 | on:
12 | release:
13 | types: [ published ]
14 |
15 | permissions:
16 | contents: read
17 |
18 | jobs:
19 | deploy:
20 |
21 | runs-on: ubuntu-latest
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Set up Python
26 | uses: actions/setup-python@v3
27 | with:
28 | python-version: '3.x'
29 | - name: Install dependencies
30 | run: |
31 | python -m pip install --upgrade pip
32 | pip install build
33 | - name: Build package
34 | run: python -m build
35 | - name: Publish package
36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
37 | with:
38 | user: __token__
39 | password: ${{ secrets.PYPI_API_TOKEN }}
40 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on:
3 | - push
4 | - pull_request
5 |
6 | jobs:
7 | testing-check:
8 | name: Run all tests with pytest
9 | runs-on: ubuntu-latest
10 | steps:
11 | # setup python
12 | - name: Checkout
13 | uses: actions/checkout@v2
14 | - name: Setup python
15 | uses: actions/setup-python@v4
16 | with:
17 | python-version: "3.x"
18 | cache: "pip"
19 | cache-dependency-path: "requirements*.txt"
20 | - name: Install modules
21 | run: pip install -r requirements.txt -r requirements-dev.txt
22 |
23 | # run pytest
24 | - name: Run pytest
25 | run: python -m pytest
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | .idea/
161 |
162 | # others
163 | prototyping
164 | prototyping/*
165 | .vscode
166 |
167 | # pyenv
168 | .python-version
169 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | exclude: ".*tests\/fixtures.*"
2 | repos:
3 | - repo: https://github.com/google/yapf
4 | rev: v0.43.0
5 | hooks:
6 | - id: yapf
7 | args: [ --diff ]
8 | - repo: https://github.com/pre-commit/mirrors-mypy
9 | rev: v1.2.0
10 | hooks:
11 | - id: mypy
12 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yaml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | version: 2
6 |
7 | build:
8 | os: ubuntu-22.04
9 | tools:
10 | python: "3.11"
11 |
12 | sphinx:
13 | configuration: docs/source/conf.py
14 |
15 | formats:
16 | - pdf
17 | - epub
18 |
19 | python:
20 | install:
21 | - requirements: docs/requirements.txt
22 | - method: pip
23 | path: .
24 |
--------------------------------------------------------------------------------
/.run/Make vhs.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.run/Sphinx Build.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.run/Sphinx Coverage .run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.run/pytest in tests.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.run/run example.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to ItsPrompt
2 |
3 | Thank you for your interest in contributing to ItsPrompt. We welcome and appreciate any contributions, whether it is
4 | code, documentation, bug reports, feature requests, or general feedback.
5 |
6 |
10 |
11 | ## How to Contribute
12 |
13 | There are many ways you can contribute to this project. Here are some guidelines to help you get started.
14 |
15 | If you want to read more about development, please refer to
16 | the [Development Guide](https://itsprompt.readthedocs.io/en/latest/development_guide/getting_started.html).
17 |
18 | ### Reporting Issues
19 |
20 | If you encounter any problems or have any suggestions for improvement, please open an issue on GitHub using the
21 | appropriate template. Be as descriptive as possible and include any relevant information such as error messages,
22 | screenshots, or steps to reproduce the issue.
23 |
24 | *If you need help with creating an Issue, join our [Discord](https://discord.gg/rP9Qke2jDs) and don't hesitate to
25 | contact us!*
26 |
27 | ### Submitting Pull Requests
28 |
29 | If you want to contribute code or documentation to this project, you can submit a pull request (PR) on GitHub. Before
30 | you do so, please make sure that:
31 |
32 | - You have tested your code locally and ensured that it works as expected (also be sure you have added tests).
33 | - You have written clear and concise commit messages and PR descriptions.
34 | - You have updated the documentation if necessary.
35 | - You have resolved any merge conflicts with the main branch.
36 |
37 | *If you need help with opening a PR, join our [Discord](https://discord.gg/rP9Qke2jDs) and don't hesitate to contact
38 | us!*
39 |
40 | To submit a PR, follow these steps:
41 |
42 | 1. Fork the repository and create a new branch from the main branch.
43 | 2. Set up your local clone using [Setting up the project for development](#setting-up-the-project-for-development).
44 | 3. Make your changes and commit them with a meaningful message.
45 | 4. Push your branch to your forked repository on GitHub.
46 | 5. Create a PR from your branch to the main branch of the original repository.
47 | 6. Wait for the project maintainers to review your PR and provide feedback or approval.
48 | 7. If requested, make any necessary changes and update your PR accordingly.
49 | 8. Once your PR is merged, delete your branch and fork.
50 |
51 | ### Setting up the project for development
52 |
53 | Before you can start working on ItsPrompt, you need to do a few things:
54 |
55 | 1. Install all required dependencies from `requirements.txt` and `requirements-dev.txt`.
56 | 2. Install our pre-commit hooks using `pre-commit install` in your main project directory.
57 |
58 | ### Providing Feedback
59 |
60 | We value your feedback and opinions on this project. You can share them with us by:
61 |
62 | - Joining the [Discussions](https://github.com/TheItsProjects/ItsPrompt/discussions) section on GitHub and starting or
63 | joining a conversation.
64 | - Joining our [Discord](https://discord.gg/rP9Qke2jDs) and letting us know what you think.
65 |
66 | ## Thank You
67 |
68 | We appreciate your time and effort in contributing to this project. We hope you enjoy using ItsPrompt and find it
69 | useful. Thank you for being part of our community!
70 |
--------------------------------------------------------------------------------
/ItsPrompt/data/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/ItsPrompt/data/__init__.py
--------------------------------------------------------------------------------
/ItsPrompt/data/checkbox.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 | from ..objects.prompts.option import Option
4 | from ..objects.prompts.options_with_separator import OptionsWithSeparator
5 | from ..objects.prompts.separator import Separator
6 | from ..objects.prompts.type import OptionsList
7 |
8 |
9 | @dataclass
10 | class CheckboxOption(Option):
11 | name: str
12 | id: str
13 | is_selected: bool
14 | is_disabled: bool
15 |
16 |
17 | def process_data(options: OptionsList) -> OptionsWithSeparator[CheckboxOption | Separator]:
18 | """
19 | Processes the given `options` and returns the processed list
20 |
21 | :param options: A list of options to process
22 | :type options: tuple[str | OptionWithId | Separator, ...]
23 | :raises TypeError: If an option is not processable, a `TypeError` will be raised
24 | :return: a list of `CheckboxOption`
25 | :rtype: list[CheckboxOption]
26 | """
27 | processed_options: list[CheckboxOption | Separator] = []
28 |
29 | # process given options
30 | for option in options:
31 | if type(option) is str:
32 | processed_options.append(CheckboxOption(name=option, id=option, is_selected=False, is_disabled=False))
33 | elif type(option) is tuple:
34 | processed_options.append(CheckboxOption(name=option[0], id=option[1], is_selected=False, is_disabled=False))
35 | elif type(option) is Separator:
36 | processed_options.append(option)
37 | else:
38 | raise TypeError('Argument is not processable')
39 |
40 | return OptionsWithSeparator(*processed_options)
41 |
--------------------------------------------------------------------------------
/ItsPrompt/data/expand.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 | from ..objects.prompts.option import Option
4 | from ..objects.prompts.options_with_separator import OptionsWithSeparator
5 | from ..objects.prompts.separator import Separator
6 | from ..objects.prompts.type import OptionsList
7 |
8 |
9 | @dataclass
10 | class ExpandOption(Option):
11 | key: str
12 | name: str
13 | id: str
14 | is_disabled: bool
15 |
16 |
17 | class ExpandOptionsWithSeparator(OptionsWithSeparator[ExpandOption | Separator]):
18 |
19 | def __init__(self, *args: ExpandOption | Separator):
20 | super().__init__(*args)
21 |
22 | def get_option(self, key: str) -> ExpandOption | None:
23 | """
24 | Get the option with the given key.
25 |
26 | :param key: The key of the option to get
27 | :type key: str
28 | :return: The option with the given key or None if not found
29 | :rtype: ExpandOption | None
30 | """
31 | for option in self.with_separators:
32 | if isinstance(option, ExpandOption) and option.key == key:
33 | return option
34 | return None
35 |
36 |
37 | def process_data(options: OptionsList) -> ExpandOptionsWithSeparator:
38 | """
39 | Processes the given `options` and returns the processed list
40 |
41 | :param options: A list of options to process
42 | :type options: tuple[str | OptionWithId | Separator, ...]
43 | :raises ValueError: If the keys are not unique
44 | :raises ValueError: If the keys are not of length 1
45 | :raises ValueError: If the keys are not ascii
46 | :raises ValueError: If the keys are using h
47 | :raises TypeError: If an option is not processable
48 | :return: A list of `ExpandOptions`
49 | :rtype: list[ExpandOption]
50 | """
51 | # check if every key is unique, otherwise return error
52 | keys = [option[0] for option in options if type(option) is not Separator] # type: ignore
53 | if len(set(keys)) < len(keys):
54 | raise ValueError('Keys must be unique!')
55 |
56 | # check that every key string is only one char long
57 | if any(
58 | [
59 | len(option[0]) > 1 or len(option[0]) < 1 for option in options if # type: ignore
60 | type(option) is not Separator
61 | ]
62 | ):
63 | raise ValueError('Keys must be of length 1!')
64 |
65 | # check that every key is ascii
66 | if any([not option[0].isascii() for option in options if type(option) is not Separator]): # type: ignore
67 | raise ValueError('Keys must be ascii!')
68 |
69 | # check if h is not assigned
70 | if any([option[0] == 'h' for option in options if type(option) is not Separator]): # type: ignore
71 | raise ValueError('The h-key is not assignable!')
72 |
73 | processed_options: list[ExpandOption] = []
74 |
75 | # process given options
76 | for option in options:
77 | if type(option) is str:
78 | # use the first letter as the key, and str as name and id
79 | processed_options.append(ExpandOption(key=option[0], name=option, id=option, is_disabled=False))
80 | elif type(option) is tuple:
81 | processed_options.append(
82 | ExpandOption(key=option[0], name=option[1], id=option[2], is_disabled=False) # type: ignore
83 | )
84 | elif type(option) is Separator:
85 | processed_options.append(option) # type: ignore
86 | else:
87 | raise TypeError('Argument is not processable')
88 |
89 | # append a help option
90 | processed_options.append(
91 | ExpandOption(key='h', name='Help Menu, list or hide all options', id='', is_disabled=False)
92 | )
93 |
94 | return ExpandOptionsWithSeparator(*processed_options)
95 |
--------------------------------------------------------------------------------
/ItsPrompt/data/select.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 | from ItsPrompt.objects.prompts.option import Option
4 | from ItsPrompt.objects.prompts.options_with_separator import OptionsWithSeparator
5 | from ItsPrompt.objects.prompts.separator import Separator
6 | from ItsPrompt.objects.prompts.type import OptionsList
7 |
8 |
9 | @dataclass
10 | class SelectOption(Option):
11 | name: str
12 | id: str
13 | is_disabled: bool
14 |
15 |
16 | def process_data(options: OptionsList) -> OptionsWithSeparator[SelectOption | Separator]:
17 | """
18 | Processes the given `options` and returns the processed list
19 |
20 | :param options: A list of options to process
21 | :type options: tuple[str | OptionWithId | Separator, ...]
22 | :raises TypeError: If an option is not processable, a `TypeError` will be raised
23 | :return: a list of `SelectOptions`
24 | :rtype: list[SelectOption]
25 | """
26 | processed_options: list[SelectOption | Separator] = []
27 |
28 | # process given options
29 | for option in options:
30 | if type(option) is str:
31 | processed_options.append(SelectOption(name=option, id=option, is_disabled=False))
32 | elif type(option) is tuple:
33 | processed_options.append(SelectOption(name=option[0], id=option[1], is_disabled=False))
34 | elif type(option) is Separator:
35 | processed_options.append(option)
36 | else:
37 | raise TypeError('Argument is not processable')
38 |
39 | return OptionsWithSeparator(*processed_options)
40 |
--------------------------------------------------------------------------------
/ItsPrompt/data/style.py:
--------------------------------------------------------------------------------
1 | import copy
2 | from dataclasses import asdict, dataclass
3 |
4 | from prompt_toolkit.styles import Style
5 |
6 |
7 | @dataclass
8 | class PromptStyle:
9 | """
10 | The style object used for styling the prompts.
11 |
12 | Empty styles will not be styled, so they appear without any styling.
13 | """
14 | question_mark: str = ''
15 | question: str = ''
16 | option: str = ''
17 | selected_option: str = ''
18 | tooltip: str = ''
19 | error: str = ''
20 | text: str = ''
21 | grayout: str = ''
22 | disabled: str = ''
23 | separator: str = ''
24 |
25 |
26 | default_style = PromptStyle(
27 | question_mark="fg:ansigreen",
28 | selected_option="fg:ansicyan",
29 | tooltip="fg:ansibrightblue bg:ansiwhite bold",
30 | error="fg:ansiwhite bg:ansired bold",
31 | grayout="fg:ansibrightblack",
32 | disabled="fg:ansibrightblack",
33 | separator="fg:ansibrightgreen",
34 | )
35 | """
36 | The default style for the prompts.
37 | """
38 |
39 |
40 | def _convert_style(style: PromptStyle) -> Style:
41 | """
42 | Converts the given `PromptStyle` to a usable `Style` object.
43 |
44 | :param style: The style to convert
45 | :type style: PromptStyle
46 | :return: The converted `Style` object
47 | :rtype: Style
48 | """
49 | return Style.from_dict(asdict(style))
50 |
51 |
52 | def create_from_default() -> PromptStyle:
53 | """
54 | Returns a copy of the :data:`default_style` which can be edited without changing the default style.
55 |
56 | :return: An editable copy of the default style
57 | :rtype: PromptStyle
58 | """
59 | return copy.deepcopy(default_style)
60 |
--------------------------------------------------------------------------------
/ItsPrompt/data/table.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import TYPE_CHECKING, Union
3 |
4 | from ..objects.prompts.type import TablePromptDict, TablePromptList
5 | from ..objects.table.table_base import TableDataBase
6 | from ..objects.table.table_dict import TableDataFromDict
7 | from ..objects.table.table_list import TableDataFromList
8 |
9 | # only import pandas and TableData if pandas is installed
10 | try:
11 | from pandas import DataFrame
12 |
13 | from ..objects.table.table_df import TableDataFromDF
14 | except ModuleNotFoundError: # pragma: no cover
15 | # pandas were not installed
16 | pass
17 |
18 | if TYPE_CHECKING: # pragma: no cover
19 | from pandas import DataFrame
20 |
21 |
22 | class Table:
23 |
24 | def __init__(self, data: Union["DataFrame", TablePromptDict, TablePromptList]) -> None:
25 | """
26 | Creates a table object for storing the drawable table instance
27 |
28 | :param data: the data to display
29 | :type data: DataFrame
30 | """
31 | # change type of all columns to str
32 | # TODO maybe later support more datatypes
33 | self.data: TableDataBase
34 | if type(data) is dict:
35 | self.data = TableDataFromDict(data)
36 | elif type(data) is list:
37 | self.data = TableDataFromList(data)
38 | elif type(data) is DataFrame:
39 | self.data = TableDataFromDF(data)
40 |
41 | # save amount of rows
42 | self.row_count = self.data.row_count
43 | self.col_count = self.data.col_count
44 |
45 | # get max cell width
46 | max_width = os.get_terminal_size().columns
47 |
48 | # get width for each cell
49 | self.cell_width = (max_width - 1) // self.col_count - 1
50 |
51 | # save current selected cell
52 | self.cur_cell = [0, 0] # ignoring header, as this can not be selected
53 |
54 | def _col_is_last(self, col) -> bool:
55 | """checks if the given column is the last column in the DataFrame"""
56 | return self.data.get_column_location(col) == self.col_count - 1
57 |
58 | def _char_if_last_else_otherchar(
59 | self,
60 | col,
61 | char: str,
62 | otherchar: str,
63 | ) -> str:
64 | """returns the `char` if the given column is the last in the DataFrame, otherwise returns the `otherchar`"""
65 | return char if self._col_is_last(col) else otherchar
66 |
67 | def get_table_as_str(self) -> str:
68 | """
69 | Returns the table as a printable string
70 |
71 | Respects the width of the terminal at time of creation of the table object. Later this may be made dynamic.
72 | """
73 |
74 | # set initial list (each entry represents a line to print)
75 | table_out = [''] * (self.row_count * 2 + 1) # 2 lines for every row + bottom border
76 |
77 | # append left border to every string
78 | table_out[0] += '┌'
79 |
80 | for i in range(1, len(table_out), 2):
81 | table_out[i] += '│'
82 | table_out[i + 1] += '├'
83 |
84 | table_out[-1] = '└'
85 |
86 | # add headers
87 | for header in self.data.columns:
88 | table_out[0] += '─' * self.cell_width + self._char_if_last_else_otherchar(header, '┐', '┬')
89 |
90 | if len(str(header)) > self.cell_width:
91 | # /\ convert the header to str in case the user did not give a header, so it is an integer
92 | header = header[:self.cell_width - 1] + '.'
93 |
94 | table_out[1] += f'{header:^{self.cell_width}}' + '│'
95 |
96 | # add values, iterate over each column
97 | for header, values in self.data.items():
98 | # iterate over each row
99 | for i, val in zip(range(2, len(table_out), 2), values): # start at 2, because first two rows are header
100 | table_out[i] += '─' * self.cell_width + self._char_if_last_else_otherchar(header, '┤', '┼')
101 | table_out[i + 1] += f'{str(val):{self.cell_width}}' + '│'
102 |
103 | table_out[-1] += '─' * self.cell_width + self._char_if_last_else_otherchar(header, '┘', '┴')
104 |
105 | return '\n'.join(table_out)
106 |
107 | def on_up(self):
108 | """when up is pressed, select same column, but row one above (or last row if current is 0)"""
109 |
110 | self.cur_cell[1] = (self.cur_cell[1] - 1) % (self.row_count - 1)
111 |
112 | def on_down(self):
113 | """when down is pressed, select same column, but row one below (or first row if current is last)"""
114 |
115 | self.cur_cell[1] = (self.cur_cell[1] + 1) % (self.row_count - 1)
116 |
117 | def on_left(self):
118 | """when left is pressed, select same row, but column one to the left (or last column if current is 0)"""
119 |
120 | self.cur_cell[0] = (self.cur_cell[0] - 1) % self.col_count
121 |
122 | def on_right(self):
123 | """when right is pressed, select same row, but column one to the right (or first column if current is last)"""
124 |
125 | self.cur_cell[0] = (self.cur_cell[0] + 1) % self.col_count
126 |
127 | def add_key(self, key: str):
128 | """add key to current selected cell"""
129 | self.data.add_key(self.cur_cell[1], self.cur_cell[0], key)
130 |
131 | def del_key(self):
132 | """remove last key from current selected cell"""
133 | self.data.del_key(self.cur_cell[1], self.cur_cell[0])
134 |
135 | def get_current_cursor_position(self) -> tuple[int, int]:
136 | """returns the current position of the cursor, relative to the top left corner character as (0, 0)"""
137 | y = self.cur_cell[1] * 2 + 3 # 3 is offset for header
138 |
139 | item_length = len(self.data.get_item_at(self.cur_cell[1], self.cur_cell[0]))
140 | # length of the item in the current cell
141 |
142 | x = (self.cell_width + 1) * self.cur_cell[0] + 1 + item_length
143 | # 1 is offset for left border, item_length is length of item
144 | return x, y
145 |
--------------------------------------------------------------------------------
/ItsPrompt/keyboard_handler.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit import Application
2 | from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
3 |
4 |
5 | def generate_key_bindings(app: type[Application]) -> KeyBindings:
6 | """
7 | Generates a KeyBindings object for use in a Prompt class
8 |
9 | The `app` is used to check whether specific functions for handling the key events exist. It has to be a Prompt
10 | object (e.g. InputPrompt).
11 |
12 | The available functions are:
13 |
14 | `on_up()`
15 |
16 | `on_down()`
17 |
18 | `on_left()`
19 |
20 | `on_right()`
21 |
22 | `on_enter()`
23 |
24 | `on_space()`
25 |
26 | `on_alt_enter()`
27 |
28 | `on_backspace()`
29 |
30 | `on_ctrl_backspace()`
31 |
32 | `on_key()`
33 |
34 | :param app: An application type to check whether one of the above functions exists
35 | :type app: type[Application]
36 | :return: An usable KeyBindings instance
37 | :rtype: KeyBindings
38 | """
39 |
40 | kb = KeyBindings()
41 |
42 | # Each of the options gets the keyboard input and runs the equivalent function in the event.app class,
43 | # if the function exists.
44 |
45 | @kb.add('c-c')
46 | def quit(event: KeyPressEvent):
47 | event.app.exit()
48 |
49 | @kb.add('up', filter=hasattr(app, 'on_up'))
50 | def up(event: KeyPressEvent):
51 | event.app.on_up() # type: ignore
52 |
53 | @kb.add('down', filter=hasattr(app, 'on_down'))
54 | def down(event: KeyPressEvent):
55 | event.app.on_down() # type: ignore
56 |
57 | @kb.add('left', filter=hasattr(app, 'on_left'))
58 | def left(event: KeyPressEvent):
59 | event.app.on_left() # type: ignore
60 |
61 | @kb.add('right', filter=hasattr(app, 'on_right'))
62 | def right(event: KeyPressEvent):
63 | event.app.on_right() # type: ignore
64 |
65 | @kb.add('enter', filter=hasattr(app, 'on_enter'))
66 | def enter(event: KeyPressEvent):
67 | event.app.on_enter() # type: ignore
68 |
69 | @kb.add('space', filter=hasattr(app, 'on_space'))
70 | def space(event: KeyPressEvent):
71 | event.app.on_space() # type: ignore
72 |
73 | @kb.add('escape', 'enter', filter=hasattr(app, 'on_alt_enter'))
74 | # Vt100 terminals convert "alt+key" to "escape,key"
75 | def alt_enter(event: KeyPressEvent):
76 | event.app.on_alt_enter() # type: ignore
77 |
78 | # backspace is mapped to ctrl-h
79 | @kb.add('c-h', filter=hasattr(app, 'on_backspace'))
80 | def backspace(event: KeyPressEvent):
81 | event.app.on_backspace() # type: ignore
82 |
83 | # This Method is used nowhere, so it is commented out. If there is ever a need to use it, it is still there.
84 | # # ctrl-backspace is mapped to ctrl-w
85 | # @kb.add('c-w', filter=hasattr(app, 'on_ctrl_backspace'))
86 | # def ctrl_backspace(event: KeyPressEvent):
87 | # event.app.on_ctrl_backspace() # type: ignore
88 |
89 | @kb.add('', filter=hasattr(app, 'on_key'))
90 | def wildcard(event: KeyPressEvent):
91 | # wildcard function, used for prompts which need standard key presses like numbers or characters
92 | event.app.on_key([key.key for key in event.key_sequence]) # type: ignore
93 |
94 | return kb
95 |
--------------------------------------------------------------------------------
/ItsPrompt/objects/prompts/option.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass
5 | class Option:
6 | pass
7 |
--------------------------------------------------------------------------------
/ItsPrompt/objects/prompts/options_with_separator.py:
--------------------------------------------------------------------------------
1 | from typing import Iterator, TypeVar, Generic
2 |
3 | from ItsPrompt.objects.prompts.option import Option
4 | from ItsPrompt.objects.prompts.separator import Separator
5 |
6 | OptionOrSeparator = TypeVar("OptionOrSeparator", bound=Option | Separator)
7 |
8 |
9 | class OptionsWithSeparator(Generic[OptionOrSeparator], list):
10 | """
11 | The `OptionsWithSeparator` class is a subclass of the built-in list class in Python.
12 |
13 | This class is designed to hold a list of arguments, but with a specific behavior:
14 | it filters out all `Separator`s. The original list including the `Separator` instances is stored
15 | in the `with_separators` attribute.
16 |
17 | :ivar with_separators: A list that holds the original arguments passed including all `Separator`s.
18 |
19 | :param args: The items for the list
20 |
21 | Note:
22 | These `Separator` instances are excluded from the parent list object. To access the
23 | original list that includes the `Separator` instances, use the `with_separators`
24 | attribute.
25 |
26 | """
27 |
28 | def __init__(self, *args: OptionOrSeparator | Separator):
29 | self.with_separators = list(args)
30 | super().__init__([x for x in args if not isinstance(x, Separator)])
31 |
32 | def with_separators_enumerate(self) -> Iterator[tuple[int, OptionOrSeparator | Separator]]:
33 | """
34 | Enumerate the items in the `OptionsWithSeperator` list, including separators.
35 |
36 | For every Separator, the index is not increased and instead -1 is returned.
37 |
38 | :return: An iterator that yields tuples containing the index and the item.
39 | """
40 | index = 0
41 |
42 | for item in self.with_separators:
43 | if type(item) is Separator:
44 | yield -1, item
45 | else:
46 | yield index, item
47 | index += 1
48 |
--------------------------------------------------------------------------------
/ItsPrompt/objects/prompts/separator.py:
--------------------------------------------------------------------------------
1 | class Separator:
2 | """
3 | Used for creating distinctive sections in the prompt types:
4 |
5 | - :meth:`~ItsPrompt.prompt.Prompt.select`
6 | - :meth:`~ItsPrompt.prompt.Prompt.raw_select`
7 | - :meth:`~ItsPrompt.prompt.Prompt.checkbox`
8 | - :meth:`~ItsPrompt.prompt.Prompt.expand`
9 |
10 | It is purely cosmetic.
11 | """
12 |
13 | def __init__(self, label: str):
14 | """
15 | Initializes an instance of the Separator class with the given text.
16 |
17 | :param label: The text to be stored.
18 | """
19 | self.label = label
20 |
--------------------------------------------------------------------------------
/ItsPrompt/objects/prompts/type.py:
--------------------------------------------------------------------------------
1 | from ItsPrompt.objects.prompts.separator import Separator
2 |
3 | OptionWithId = tuple[str, str, str | None]
4 | """
5 | The OptionWithId tuple is used to store an option, its id and an optional key (for expand prompt).
6 |
7 | For all prompts excluding :meth:`~ItsPrompt.prompt.Prompt.expand`, the tuple is structured as follows
8 |
9 | - The first element is the displayed option
10 | - The second element is the id of the option
11 |
12 | For the :meth:`~ItsPrompt.prompt.Prompt.expand` prompt, the tuple is structured as follows:
13 |
14 | - The first element is the key of the option
15 | - The second element is the displayed option
16 | - The third element is the id of the option
17 | """
18 |
19 | OptionsList = tuple[str | OptionWithId | Separator, ...]
20 | """
21 | Different types of options that can be used in a prompt.
22 |
23 | Can be given by either:
24 |
25 | - A :class:`str`, which is the displayed option and its id
26 | - A :class:`tuple` containing the displayed option, its id and an optional key (for expand prompt)
27 | - A :class:`~ItsPrompt.objects.prompts.separator.Separator` instance
28 | """
29 |
30 | TablePromptList = list[list[str]]
31 | """
32 | A type hint for the :class:`list` structure used to represent a table prompt.
33 |
34 | Each inner :class:`list` represents a row in the table.
35 |
36 | The cells are represented by the :class:`str` type.
37 |
38 | """
39 |
40 | TablePromptDict = dict[str, list[str]]
41 | """
42 | A type hint for the :class:`dict` structure used to represent a table prompt.
43 |
44 | The keys are the column names and the values are the cells in the column.
45 | """
46 |
47 | CompletionDict = dict[str, "CompletionDict | None"]
48 | """
49 | A type hint for the :class:`dict` structure used to represent the completion dictionary.
50 | """
51 |
--------------------------------------------------------------------------------
/ItsPrompt/objects/table/table_base.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Generator
3 |
4 |
5 | class TableDataBase(ABC): # pragma: no cover
6 |
7 | @property
8 | @abstractmethod
9 | def row_count(self) -> int:
10 | """Returns the number of rows in the table, including the header"""
11 | pass
12 |
13 | @property
14 | @abstractmethod
15 | def col_count(self) -> int:
16 | """Returns the number of columns in the table"""
17 | pass
18 |
19 | @property
20 | @abstractmethod
21 | def columns(self) -> tuple[str, ...]:
22 | """Returns the column names in the table (the first row, headers)"""
23 | pass
24 |
25 | @abstractmethod
26 | def items(self) -> Generator[tuple[str, tuple[str, ...]], None, None]:
27 | """Gets all the items in the table (each tuple contains the header and the values)"""
28 | pass
29 |
30 | @abstractmethod
31 | def get_item_at(self, row: int, col: int) -> str:
32 | """Returns the item at the given position in the table"""
33 | pass
34 |
35 | @abstractmethod
36 | def set_item_at(self, row: int, col: int, val: str) -> None:
37 | """Sets the item at the given position in the table"""
38 | pass
39 |
40 | @abstractmethod
41 | def add_key(self, row: int, col: int, key: str):
42 | """Adds the key to the given cell"""
43 | pass
44 |
45 | @abstractmethod
46 | def del_key(self, row: int, col: int):
47 | """Deletes the key from the given cell"""
48 | pass
49 |
50 | @abstractmethod
51 | def get_column_location(self, val: str) -> int:
52 | """Returns the location of the column in the table"""
53 | pass
54 |
55 | @abstractmethod
56 | def get_data(self):
57 | """Returns the actual data"""
58 | pass
59 |
--------------------------------------------------------------------------------
/ItsPrompt/objects/table/table_df.py:
--------------------------------------------------------------------------------
1 | from typing import Generator
2 |
3 | from pandas import DataFrame
4 |
5 | from .table_base import TableDataBase
6 |
7 |
8 | class TableDataFromDF(TableDataBase):
9 |
10 | def __init__(self, data: DataFrame) -> None:
11 | self.data = data.astype("str") # convert all values to str
12 | self.data = self.data.rename(columns=lambda x: str(x)) # convert all headers to str
13 |
14 | @property
15 | def row_count(self) -> int:
16 | return len(self.data) + 1
17 |
18 | @property
19 | def col_count(self) -> int:
20 | return len(self.data.columns)
21 |
22 | @property
23 | def columns(self) -> tuple[str, ...]:
24 | return tuple(self.data.columns)
25 |
26 | def items(self) -> Generator[tuple[str, tuple[str, ...]], None, None]:
27 | for header, values in self.data.items():
28 | yield str(header), tuple(values)
29 |
30 | def get_item_at(self, row: int, col: int) -> str:
31 | return self.data.iat[row, col]
32 |
33 | def set_item_at(self, row: int, col: int, val: str) -> None:
34 | self.data.iat[row, col] = val
35 |
36 | def add_key(self, row: int, col: int, key: str):
37 | new = self.get_item_at(row, col) + key
38 | self.set_item_at(row, col, new)
39 |
40 | def del_key(self, row: int, col: int):
41 | new = self.get_item_at(row, col)[:-1]
42 | self.set_item_at(row, col, new)
43 |
44 | def get_column_location(self, val: str) -> int:
45 | return self.data.columns.get_loc(val)
46 |
47 | def get_data(self) -> DataFrame:
48 | return self.data
49 |
--------------------------------------------------------------------------------
/ItsPrompt/objects/table/table_dict.py:
--------------------------------------------------------------------------------
1 | from typing import Generator
2 |
3 | from .table_base import TableDataBase
4 | from ..prompts.type import TablePromptDict
5 |
6 |
7 | class TableDataFromDict(TableDataBase):
8 |
9 | def __init__(self, data: TablePromptDict) -> None:
10 | # make sure every list has same length
11 | lengths = set([len(t) for t in data.values()])
12 | if len(lengths) != 1:
13 | raise ValueError("Dictionary columns (lists) must be same length!")
14 |
15 | self.data = data
16 |
17 | @property
18 | def row_count(self) -> int:
19 | return len(next(iter(self.data.values()))) + 1
20 |
21 | @property
22 | def col_count(self) -> int:
23 | return len(self.data)
24 |
25 | @property
26 | def columns(self) -> tuple[str, ...]:
27 | return tuple(self.data.keys())
28 |
29 | def items(self) -> Generator[tuple[str, tuple[str, ...]], None, None]:
30 | for header, values in self.data.items():
31 | yield str(header), tuple(values)
32 |
33 | def get_item_at(self, row: int, col: int) -> str:
34 | # get the name of the column to edit (header)
35 | col_name = self.columns[col]
36 |
37 | return self.data[col_name][row]
38 |
39 | def set_item_at(self, row: int, col: int, val: str) -> None:
40 | # get the name of the column to edit (header)
41 | col_name = self.columns[col]
42 |
43 | self.data[col_name][row] = val
44 |
45 | def add_key(self, row: int, col: int, key: str):
46 | new = self.get_item_at(row, col) + key
47 | self.set_item_at(row, col, new)
48 |
49 | def del_key(self, row: int, col: int):
50 | new = self.get_item_at(row, col)[:-1]
51 | self.set_item_at(row, col, new)
52 |
53 | def get_column_location(self, val: str) -> int:
54 | return self.columns.index(val)
55 |
56 | def get_data(self) -> TablePromptDict:
57 | return self.data
58 |
--------------------------------------------------------------------------------
/ItsPrompt/objects/table/table_list.py:
--------------------------------------------------------------------------------
1 | from .table_dict import TableDataFromDict
2 | from ..prompts.type import TablePromptDict, TablePromptList
3 |
4 |
5 | class TableDataFromList(TableDataFromDict):
6 |
7 | def __init__(self, data: TablePromptList):
8 | # convert list to dict
9 | dict_data: TablePromptDict = {f"{col}": rows for col, rows in enumerate(data)}
10 |
11 | super().__init__(dict_data)
12 |
13 | def get_data(self) -> TablePromptList: # type: ignore
14 | # convert dict to list
15 | list_data: TablePromptList = list(self.data.values())
16 |
17 | return list_data
18 |
--------------------------------------------------------------------------------
/ItsPrompt/prompt.py:
--------------------------------------------------------------------------------
1 | """
2 | ItsPrompt
3 | =========
4 |
5 | created by ItsNameless
6 |
7 | :copyright: (c) 2023-present ItsNameless
8 | :license: MIT, see LICENSE for more details.
9 | """
10 |
11 | # mypy: disable-error-code=return-value
12 |
13 | from typing import Callable, TYPE_CHECKING, Union
14 |
15 | from prompt_toolkit import HTML
16 | from prompt_toolkit.buffer import Buffer
17 | from prompt_toolkit.completion import Completer
18 | from prompt_toolkit.layout.containers import (
19 | Float,
20 | FloatContainer,
21 | HSplit,
22 | VSplit,
23 | Window,
24 | )
25 | from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
26 | from prompt_toolkit.layout.layout import Layout
27 | from prompt_toolkit.layout.menus import (
28 | CompletionsMenu,
29 | MultiColumnCompletionsMenu,
30 | )
31 |
32 | from .data.style import PromptStyle, _convert_style, default_style
33 | from .keyboard_handler import generate_key_bindings
34 | from .objects.prompts.type import CompletionDict, TablePromptDict, TablePromptList, OptionsList
35 | from .prompts.checkbox import CheckboxPrompt
36 | from .prompts.confirm import ConfirmPrompt
37 | from .prompts.expand import ExpandPrompt
38 | from .prompts.input import InputPrompt
39 | from .prompts.raw_select import RawSelectPrompt
40 | from .prompts.select import SelectPrompt
41 | from .prompts.table import TablePrompt
42 |
43 | if TYPE_CHECKING: # pragma: no cover
44 | from pandas import DataFrame
45 |
46 |
47 | class Prompt:
48 | """
49 | # Modern python prompter
50 |
51 | This tool is used to ask the user questions, the fancy way.
52 |
53 | Usage:
54 | ```
55 | answer = Prompt.checkbox('option 1', 'option 2')
56 | ```
57 | """
58 |
59 | @classmethod
60 | def select(
61 | cls,
62 | question: str,
63 | options: OptionsList,
64 | default: str | None = None,
65 | disabled: tuple[str, ...] | None = None,
66 | style: PromptStyle | None = None,
67 | ) -> str:
68 | """
69 | Ask the user for selecting **one** of the given `options`.
70 |
71 | This method shows the question alongside the `options` as a nice list. The user has the ability to use the
72 | up, down and enter keys to navigate between the options and select the one that is right.
73 |
74 | The `options` are either a string, which is used as the display value and the id, or a tuple[str, str],
75 | where the first string is the display value and the second is the option's id.
76 |
77 | :param question: The question to display
78 | :param options: A list of possible options
79 | :param default: The id of the default option to select (empty or None if the first should be default), defaults to None
80 | :param disabled: A list of ids, which should be disabled by default (empty if None)
81 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None
82 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised
83 | :return: The id of the selected option
84 | :rtype: str
85 | """
86 | app = SelectPrompt(
87 | question,
88 | options,
89 | default,
90 | disabled,
91 | layout=Layout(
92 | HSplit(
93 | [
94 | Window(FormattedTextControl(), always_hide_cursor=True),
95 | Window(
96 | FormattedTextControl(HTML('Use UP, DOWN to select, ENTER to submit')),
97 | char=' ',
98 | style='class:tooltip',
99 | height=1
100 | )
101 | ]
102 | )
103 | ),
104 | key_bindings=generate_key_bindings(SelectPrompt),
105 | erase_when_done=True,
106 | style=_convert_style(style) if style else _convert_style(default_style),
107 | )
108 | ans = app.prompt()
109 | if ans == None:
110 | raise KeyboardInterrupt()
111 | return ans
112 |
113 | @classmethod
114 | def raw_select(
115 | cls,
116 | question: str,
117 | options: OptionsList,
118 | default: str | None = None,
119 | disabled: tuple[str, ...] | None = None,
120 | allow_keyboard: bool = False,
121 | style: PromptStyle | None = None,
122 | ) -> str:
123 | """
124 | Ask the user for selecting **one** of the given `options`.
125 |
126 | This method shows the question alongside the `options` as a nice list. The user needs to type the index of
127 | the answer. If `allow_keyboard` is given, the user may use the keyboard as in the `select()` method.
128 |
129 | The `options` are either a string, which is used as the display value and the id, or a tuple[str, str],
130 | where the first string is the display value and the second is the option's id.
131 |
132 | :param question: The question to display
133 | :param options: A list of possible options
134 | :param default: The id of the default option to select (empty or None if the first should be default), defaults to None
135 | :param disabled: A list of ids, which should be disabled by default (empty if None)
136 | :param allow_keyboard: Whether the user should be able to select the answer with up and down, defaults to False
137 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None
138 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised
139 | :return: The id of the selected option
140 | :rtype: str
141 | """
142 | app = RawSelectPrompt(
143 | question,
144 | options,
145 | default,
146 | disabled,
147 | allow_keyboard,
148 | layout=Layout(
149 | HSplit(
150 | [
151 | Window(FormattedTextControl(), always_hide_cursor=True),
152 | Window(
153 | FormattedTextControl(HTML('Type the INDEX of your selection, ENTER to submit')),
154 | char=' ',
155 | style='class:tooltip',
156 | height=1
157 | )
158 | ]
159 | )
160 | ),
161 | key_bindings=generate_key_bindings(RawSelectPrompt),
162 | erase_when_done=True,
163 | style=_convert_style(style) if style else _convert_style(default_style),
164 | )
165 | ans = app.prompt()
166 | if ans is None:
167 | raise KeyboardInterrupt()
168 | return ans
169 |
170 | @classmethod
171 | def expand(
172 | cls,
173 | question: str,
174 | options: OptionsList,
175 | default: str | None = None,
176 | disabled: tuple[str, ...] | None = None,
177 | allow_keyboard: bool = False,
178 | style: PromptStyle | None = None,
179 | ) -> str:
180 | """
181 | Ask the user for selecting **one** of the given `options`.
182 |
183 | The user needs to type the key of the option. If the user types `h`, all options will be shown.
184 |
185 | The `options` are either a string, where `s[0]` will be the key to select and the string will be used as name
186 | and id, or a tuple[str, str, str] where `t[0]` will be the key, `t[1]` the name and `t[2]` the id of the option.
187 |
188 | Every key must be a unique ascii character and of length 1, and there may not be a key assigned to `h`.
189 |
190 | :param question: The question to display
191 | :param options: A list of possible options
192 | :param default: The id of the default option to select (empty or None if `h` should be default), defaults to None
193 | :param disabled: A list of ids, which should be disabled by default (empty if None)
194 | :param allow_keyboard: Whether the user should be able to select the answer with up and down, defaults to False
195 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None
196 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised
197 | :return: The id of the selected option
198 | :rtype: str
199 | """
200 | app = ExpandPrompt(
201 | question,
202 | options,
203 | default,
204 | disabled,
205 | allow_keyboard,
206 | layout=Layout(
207 | HSplit(
208 | [
209 | Window(FormattedTextControl(), always_hide_cursor=True),
210 | Window(
211 | FormattedTextControl(
212 | HTML('Type the KEY for your selection, ENTER to submit (use h to show all options)')
213 | ),
214 | char=' ',
215 | style='class:tooltip',
216 | height=1
217 | )
218 | ]
219 | )
220 | ),
221 | key_bindings=generate_key_bindings(ExpandPrompt),
222 | erase_when_done=True,
223 | style=_convert_style(style) if style else _convert_style(default_style),
224 | )
225 | ans = app.prompt()
226 | if ans is None:
227 | raise KeyboardInterrupt()
228 | return ans
229 |
230 | @classmethod
231 | def checkbox(
232 | cls,
233 | question: str,
234 | options: OptionsList,
235 | pointer_at: int | None = None,
236 | default_checked: tuple[str, ...] | None = None,
237 | disabled: tuple[str, ...] | None = None,
238 | min_selections: int = 0,
239 | style: PromptStyle | None = None,
240 | ) -> list[str]:
241 | """
242 | Ask the user for selecting **multiple** of the given `options`.
243 |
244 | The `options` will be shown as a nice list. The user may navigate with up and down, select or deselect with
245 | space and submit with enter.
246 |
247 | The `options` are either a string, which is used as the display value and the id, or a tuple[str, str],
248 | where the first string is the display value and the second is the option's id.
249 |
250 | :param question: The question to display
251 | :param options: A list of possible options
252 | :param pointer_at: A 0-indexed value, where the pointer should start (0 if None), defaults to None
253 | :param default_checked: A list of ids, which should be checked by default (empty if None)
254 | :param disabled: A list of ids, which should be disabled by default (empty if None)
255 | :param min_selections: A minimum amount of options that need to be checked before submitting (prohibits the user of submitting, if not enough are checked; 0 if None)
256 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None
257 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised
258 | :return: The ids of the selected options
259 | :rtype: list[str]
260 | """
261 | app = CheckboxPrompt(
262 | question,
263 | options,
264 | pointer_at,
265 | default_checked,
266 | disabled,
267 | min_selections,
268 | layout=Layout(
269 | HSplit(
270 | [
271 | Window(FormattedTextControl(), always_hide_cursor=True),
272 | Window(
273 | FormattedTextControl(
274 | HTML('Use UP, DOWN to change selection, SPACE to select, ENTER to submit')
275 | ),
276 | char=' ',
277 | style='class:tooltip',
278 | height=1
279 | )
280 | ]
281 | )
282 | ),
283 | key_bindings=generate_key_bindings(CheckboxPrompt),
284 | erase_when_done=True,
285 | style=_convert_style(style) if style else _convert_style(default_style),
286 | )
287 | ans = app.prompt()
288 | if ans == None:
289 | raise KeyboardInterrupt()
290 | return ans
291 |
292 | @classmethod
293 | def confirm(
294 | cls,
295 | question: str,
296 | default: bool | None = None,
297 | style: PromptStyle | None = None,
298 | ) -> bool:
299 | """
300 | Ask the user for confirming or denying your prompt.
301 |
302 | The user needs to type "y", "n" or enter (only if default is given).
303 |
304 | If `default` is `True`, the prompt will be in the style of (Y/n).
305 |
306 | If `default` is `False`, the prompt will be in the style of (n/Y).
307 |
308 | If `default` is `None` (or not given), the prompt will be in the style of (y/n). In this case, the user may
309 | not use enter to submit the default, as there is no default given.
310 |
311 | :param question: The question to display
312 | :type question: str
313 | :param default: The default answer to select when pressing enter, defaults to None
314 | :type default: bool | None, optional
315 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None
316 | :type style: PromptStyle | None, optional
317 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised
318 | :return: Whether the user selected "y" or "n"
319 | :rtype: bool
320 | """
321 | app = ConfirmPrompt(
322 | question,
323 | default,
324 | layout=Layout(
325 | HSplit(
326 | [
327 | Window(FormattedTextControl(), always_hide_cursor=True),
328 | Window(
329 | FormattedTextControl(HTML('Press Y or N, ENTER if default value is available')),
330 | char=' ',
331 | style='class:tooltip',
332 | height=1
333 | )
334 | ]
335 | )
336 | ),
337 | key_bindings=generate_key_bindings(ConfirmPrompt),
338 | erase_when_done=True,
339 | style=_convert_style(style) if style else _convert_style(default_style),
340 | )
341 |
342 | ans = app.prompt()
343 | if ans == None:
344 | raise KeyboardInterrupt()
345 | return ans
346 |
347 | @classmethod
348 | def input(
349 | cls,
350 | question: str,
351 | default: str | None = None,
352 | multiline: bool = False,
353 | show_symbol: str | None = None,
354 | validate: Callable[[str], str | bool | None] | None = None,
355 | completions: list[str] | CompletionDict | None = None,
356 | completer: Completer | None = None,
357 | completion_show_multicolumn: bool = False,
358 | style: PromptStyle | None = None,
359 | ) -> str:
360 | """
361 | Ask the user for typing an input.
362 |
363 | If :data:`default` is given, it will be returned if enter was pressed and no input was given by the user. If the user
364 | writes an input, the :data:`default` will be overwritten.
365 |
366 | If :data:`multiline` is activated, enter will not submit, but rather create a newline. Use ``alt+enter`` to submit.
367 |
368 | If :data:`show_symbol` is given, all chars (except newlines) will be replaced with this character in the interface.
369 | The result will still be the input the user typed, it just will not appear in the CLI. This is useful for
370 | password inputs.
371 |
372 | :data:`validate` takes a function which receives a :class:`str` (the current input of the user) and may
373 | return :class:`None`, a :class:`str` or simply a boolean value.
374 |
375 | If the function returns :class:`None` (or ``True``), the prompt may assume that the input is
376 | valid.
377 |
378 | If it returns a :class:`str`, this will be the error shown to the user. If it returns ``False``, the error
379 | shown will simply be a general error statement without additional information. The user will not be able to
380 | submit the input, if :data:`validate` returns an error.
381 |
382 | :data:`completions` may be a list of possible completion strings or a nested dictionary where the key is a
383 | completion string and the value is a new dict in the same style (more in the README.md).
384 |
385 | You can use your own :class:`Completer` as well (more in the README.md).
386 |
387 | :data:`completions` **and** :data:`completer` **are mutually exclusive!** You may not use both. If you use a :data:`completer`, you can not use
388 | :data:`show_symbol`!
389 |
390 | :param question: The question to display
391 | :type question: str
392 | :param default: The default value to fill in, defaults to None
393 | :type default: str | None, optional
394 | :param multiline: Whether to allow the user to type multiple lines, defaults to False
395 | :type multiline: bool, optional
396 | :param show_symbol: A symbol to show instead of the users input, defaults to None
397 | :type show_symbol: str | None, optional
398 | :param validate: A function to check the users input in real-time, defaults to None
399 | :type validate: Callable[[str], str | bool | None] | None, optional
400 | :param completions: The completions to use, defaults to None
401 | :type completions: list[str] | CompletionDict | None, optional
402 | :param completer: A completer to use, defaults to None
403 | :type completer: Completer | None, optional
404 | :param completion_show_multicolumn: if True, shows completions as multiple columns, defaults to False
405 | :type completion_show_multicolumn: bool, optional
406 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None
407 | :type style: PromptStyle | None, optional
408 | :raises KeyboardInterrupt: When the user presses ctrl-c, :class:`KeyboardInterrupt` will be raised
409 | :return: The input of the user
410 | :rtype: str
411 | """
412 |
413 | # extracting the body, so we can display a floating auto completion field
414 | body = HSplit(
415 | [
416 | VSplit(
417 | [
418 | Window(
419 | FormattedTextControl(),
420 | always_hide_cursor=True,
421 | dont_extend_width=True,
422 | ),
423 | Window(BufferControl(Buffer(complete_while_typing=True))),
424 | # the completer will be passed in the Application class
425 | ]
426 | ),
427 | Window(
428 | FormattedTextControl(HTML(f'Type your answer, {"ALT+ENTER" if multiline else "ENTER"} to submit')),
429 | char=' ',
430 | style='class:tooltip',
431 | height=1,
432 | )
433 | ]
434 | )
435 |
436 | app = InputPrompt(
437 | question,
438 | default,
439 | multiline,
440 | show_symbol,
441 | validate,
442 | completions,
443 | completer,
444 | layout=Layout(
445 | FloatContainer(
446 | content=body,
447 | floats=[
448 | Float(
449 | MultiColumnCompletionsMenu(show_meta=False)
450 | if completion_show_multicolumn else CompletionsMenu(),
451 | xcursor=True,
452 | ycursor=True,
453 | )
454 | ]
455 | )
456 | ),
457 | key_bindings=generate_key_bindings(InputPrompt),
458 | erase_when_done=True,
459 | style=_convert_style(style) if style else _convert_style(default_style),
460 | )
461 |
462 | ans = app.prompt()
463 | if ans is None:
464 | raise KeyboardInterrupt()
465 | return ans
466 |
467 | @classmethod
468 | def table(
469 | cls,
470 | question: str,
471 | data: Union["DataFrame", TablePromptDict, TablePromptList],
472 | style: PromptStyle | None = None,
473 | ) -> Union["DataFrame", TablePromptDict, TablePromptList]:
474 | """
475 | Ask the user for filling out the displayed table.
476 |
477 | This method shows the question alongside a table, which the user may navigate with the arrow keys. The user
478 | can use the up, down and enter keys to navigate between the options and change the text in
479 | each cell.
480 |
481 | The `data` is either a :class:`pandas.DataFrame`, a :class:`list` or a :class:`dict` (more in the README.md).
482 |
483 | :param question: The question to display
484 | :type question: str
485 | :param data: The data to display
486 | :type data: DataFrame | TablePromptDict | TablePromptList
487 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None
488 | :type style: PromptStyle | None, optional
489 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised
490 | :return: The id of the selected option
491 | :rtype: DataFrame | TablePromptDict | TablePromptList
492 | """
493 | app = TablePrompt(
494 | question,
495 | data,
496 | layout=Layout(
497 | HSplit(
498 | [
499 | Window(FormattedTextControl()),
500 | Window(
501 | FormattedTextControl(
502 | HTML(
503 | 'Use UP, DOWN, LEFT, RIGHT to select a cell, TYPE to add char, BACKSPACE to '
504 | 'delete char, ENTER to submit'
505 | )
506 | ),
507 | char=' ',
508 | style='class:tooltip',
509 | height=1
510 | )
511 | ]
512 | )
513 | ),
514 | key_bindings=generate_key_bindings(TablePrompt),
515 | erase_when_done=True,
516 | style=_convert_style(style) if style else _convert_style(default_style),
517 | )
518 | ans = app.prompt()
519 | # if type(ans) is type(None):
520 | if ans is None:
521 | raise KeyboardInterrupt()
522 | return ans # type: ignore
523 |
--------------------------------------------------------------------------------
/ItsPrompt/prompts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/ItsPrompt/prompts/__init__.py
--------------------------------------------------------------------------------
/ItsPrompt/prompts/checkbox.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit import Application, HTML
2 | from prompt_toolkit.layout.containers import Window
3 | from prompt_toolkit.layout.controls import FormattedTextControl
4 |
5 | from ..data.checkbox import process_data
6 | from ..objects.prompts.separator import Separator
7 | from ..objects.prompts.type import OptionsList
8 |
9 |
10 | class CheckboxPrompt(Application):
11 | CHECKED_SIGN = '\u25cf'
12 | UNCHECKED_SIGN = '\u25cb'
13 |
14 | def __init__(
15 | self,
16 | question: str,
17 | options: OptionsList,
18 | pointer_at: int | None = None,
19 | default_checked: tuple[str, ...] | None = None,
20 | disabled: tuple[str, ...] | None = None,
21 | min_selections: int = 0,
22 | *args,
23 | **kwargs,
24 | ):
25 | super().__init__(*args, **kwargs)
26 |
27 | # get prompt content box
28 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore
29 |
30 | # save toolbar window and box (for showing errors)
31 | self.toolbar_window: Window = self.layout.container.get_children()[1] # type: ignore
32 | self.toolbar_content: FormattedTextControl = self.layout.container.get_children()[1].content # type: ignore
33 |
34 | # save standard toolbar content
35 | self.toolbar_content_default_text = self.toolbar_content.text
36 |
37 | # save whether error is currently shown
38 | self.is_error = False
39 |
40 | # save question
41 | self.question = question
42 |
43 | # save min selections
44 | self.min_selections = min_selections
45 |
46 | # process options
47 | self.options = process_data(options)
48 |
49 | # default check default option (or none if not given)
50 | if default_checked is not None:
51 | was_set = 0 # Keeping track of options which where selected.
52 | # If was_set is not as big as len(default_checked),
53 | # then we know that there was an invalid option which could not be checked,
54 | # so we raise an error.
55 | for option in self.options:
56 | if option.id in default_checked:
57 | option.is_selected = True
58 | was_set += 1
59 |
60 | if was_set != len(default_checked):
61 | raise ValueError('At least one of the given default_checked values is invalid.')
62 |
63 | # disable options (or none if not given)
64 | if disabled is not None:
65 | was_set = 0 # Keeping track of options which where disabled.
66 | # If was_set is not as big as len(disabled),
67 | # then we know that there was an invalid option which could not be disabled,
68 | # so we raise an error.
69 | for option in self.options:
70 | if option.id in disabled:
71 | option.is_disabled = True
72 | was_set += 1
73 |
74 | if was_set != len(disabled):
75 | raise ValueError("At least one of the given disabled values is invalid.")
76 |
77 | # set pointer selection
78 | self.selection = pointer_at if pointer_at else 0
79 |
80 | # if the current selection is disabled, we will skip it
81 | while self.options[self.selection].is_disabled:
82 | self.selection = (self.selection + 1) % len(self.options)
83 |
84 | def update(self):
85 | """update prompt content"""
86 | content = f'[?] {self.question}:'
87 |
88 | for i, option in self.options.with_separators_enumerate():
89 |
90 | if type(option) is Separator:
91 | content += f"\n{option.label}"
92 | continue
93 |
94 | disabled = ("", "") if option.is_disabled else ("", "") # type: ignore
95 | # Disabled tags will only be inserted if the option is disabled,
96 | # otherwise there will only be an empty string inserted.
97 | selected = self.__class__.CHECKED_SIGN if option.is_selected else self.__class__.UNCHECKED_SIGN # type: ignore
98 |
99 | if i == self.selection:
100 | content += f"\n{disabled[0]} > {selected} {option.name}{disabled[1]}" # type: ignore
101 | else:
102 | content += f"\n{disabled[0]}{disabled[1]}" # type: ignore
103 |
104 | self.prompt_content.text = HTML(content)
105 |
106 | # show error, if error should be shown, else show normal prompt
107 | if not self.is_error:
108 | # show normal prompt, change style to standard toolbar
109 | self.toolbar_content.text = self.toolbar_content_default_text
110 | self.toolbar_window.style = 'class:tooltip'
111 | else: # pragma: no cover
112 | # show error prompt and error style
113 | # the only error that might occur is that not enough options are selected
114 | self.toolbar_content.text = f'ERROR: a minimum of {self.min_selections} options need to be ' \
115 | f'selected!'
116 | self.toolbar_window.style = 'class:error'
117 |
118 | def prompt(self) -> list[str] | None:
119 | """start the application, returns the return value"""
120 | self.update()
121 | out: list[str] | None = self.run()
122 |
123 | return out
124 |
125 | def on_up(self):
126 | """when up is pressed, the previous indexed option will be selected"""
127 | self.selection = (self.selection - 1) % len(self.options)
128 |
129 | # if the current selection is disabled, we will skip it
130 | while self.options[self.selection].is_disabled:
131 | self.selection = (self.selection - 1) % len(self.options)
132 |
133 | # reset error
134 | self.is_error = False
135 |
136 | self.update()
137 |
138 | def on_down(self):
139 | """when down is pressed, the next indexed option will be selected"""
140 | self.selection = (self.selection + 1) % len(self.options)
141 |
142 | # if the current selection is disabled, we will skip it
143 | while self.options[self.selection].is_disabled:
144 | self.selection = (self.selection + 1) % len(self.options)
145 |
146 | # reset error
147 | self.is_error = False
148 |
149 | self.update()
150 |
151 | def on_space(self):
152 | """when space is pressed, select or deselect current selection"""
153 | self.options[self.selection].is_selected = not self.options[self.selection].is_selected
154 |
155 | # reset error
156 | self.is_error = False
157 |
158 | self.update()
159 |
160 | def on_enter(self):
161 | # get selected options
162 | selected_options: list[str] = []
163 |
164 | for option in self.options:
165 | if option.is_selected:
166 | selected_options.append(option.id)
167 |
168 | # make sure that enough options are selected
169 | if len(selected_options) < self.min_selections: # pragma: no cover
170 | # show error
171 | self.is_error = True
172 | self.update()
173 | return
174 |
175 | self.exit(result=selected_options)
176 |
--------------------------------------------------------------------------------
/ItsPrompt/prompts/confirm.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit import Application, HTML
2 | from prompt_toolkit.layout.containers import Window
3 | from prompt_toolkit.layout.controls import FormattedTextControl
4 |
5 |
6 | class ConfirmPrompt(Application):
7 |
8 | def __init__(
9 | self,
10 | question: str,
11 | default: bool | None = None,
12 | *args,
13 | **kwargs,
14 | ):
15 | super().__init__(*args, **kwargs)
16 |
17 | # get prompt content box
18 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore
19 |
20 | # save toolbar window and box (for showing errors)
21 | self.toolbar_window: Window = self.layout.container.get_children()[1] # type: ignore
22 | self.toolbar_content: FormattedTextControl = self.layout.container.get_children()[1].content # type: ignore
23 |
24 | # save standard toolbar content
25 | self.toolbar_content_default_text = self.toolbar_content.text
26 |
27 | # save whether error is currently shown
28 | self.is_error = False
29 |
30 | # save question
31 | self.question = question
32 |
33 | # save default selection
34 | self.default = default
35 |
36 | def update(self):
37 | """update prompt content"""
38 | content = f'[?] {self.question}: (' \
39 | f'{"Y" if self.default == True else "y"}/{"N" if self.default == False else "n"})'
40 |
41 | self.prompt_content.text = HTML(content)
42 |
43 | # show error, if error should be shown, else show normal prompt
44 | if not self.is_error:
45 | # show normal prompt, change style to standard toolbar
46 | self.toolbar_content.text = self.toolbar_content_default_text
47 | self.toolbar_window.style = 'class:tooltip'
48 | else: # pragma: no cover
49 | # show error prompt and error style
50 | # the only error that might occur is that not enough options are selected
51 | self.toolbar_content.text = f'ERROR: a selection must be made!'
52 | self.toolbar_window.style = 'class:error'
53 |
54 | def prompt(self) -> bool | None:
55 | """start the application, returns the return value"""
56 | self.update()
57 | out: bool | None = self.run()
58 |
59 | return out
60 |
61 | def on_key(self, key_sequence: list[str]):
62 | """when Y or N is pressed, select value and submit"""
63 | key = key_sequence[0]
64 |
65 | # return if key is not an available key
66 | if not key in ['y', 'n']:
67 | return
68 |
69 | # exit with answer
70 | self.exit(result=key == 'y')
71 |
72 | def on_enter(self):
73 | # if no default is present, user is not able to just submit
74 | if self.default is None: # pragma: no cover
75 | self.is_error = True
76 | self.update()
77 | return
78 |
79 | self.exit(result=self.default)
80 |
--------------------------------------------------------------------------------
/ItsPrompt/prompts/expand.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit import Application, HTML
2 | from prompt_toolkit.layout.controls import FormattedTextControl
3 |
4 | from ..data.expand import process_data
5 | from ..objects.prompts.separator import Separator
6 | from ..objects.prompts.type import OptionsList
7 |
8 |
9 | class ExpandPrompt(Application):
10 |
11 | def __init__(
12 | self,
13 | question: str,
14 | options: OptionsList,
15 | default: str | None = None,
16 | disabled: tuple[str, ...] | None = None,
17 | allow_keyboard: bool = False,
18 | *args,
19 | **kwargs,
20 | ):
21 | super().__init__(*args, **kwargs)
22 |
23 | # get prompt content box
24 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore
25 |
26 | # save question
27 | self.question = question
28 |
29 | # save keyboard option
30 | self.allow_keyboard = allow_keyboard
31 |
32 | # save whether view is expanded or not
33 | self.is_expanded = False
34 |
35 | # process options
36 | self.options = process_data(options)
37 |
38 | # save a string with all keys
39 | self.keys = ''.join([option.key for option in self.options])
40 |
41 | # save disabled keys
42 | self.disabled = [key[0] for key in disabled] if disabled else ()
43 |
44 | # default select default option (or "help" if not given)
45 | if default is None:
46 | self.selection = 'h'
47 |
48 | else:
49 | for i, option in enumerate(self.options):
50 | if option.id == default:
51 | self.selection = option.key
52 | break
53 | else:
54 | raise ValueError('Default value is not a valid id.')
55 |
56 | # if default is disabled, raise an error
57 | if disabled and default in disabled:
58 | raise ValueError("Default value can not be disabled.")
59 |
60 | # disable options (or none if not given)
61 | if disabled is not None:
62 | was_set = 0 # Keeping track of options which where disabled.
63 | # If was_set is not as big as len(disabled),
64 | # then we know that there was an invalid option which could not be disabled,
65 | # so we raise an error.
66 | for option in self.options:
67 | if option.id in disabled:
68 | option.is_disabled = True
69 | was_set += 1
70 |
71 | if was_set != len(disabled):
72 | raise ValueError("At least one of the given disabled values is invalid.")
73 |
74 | def update(self):
75 | """update prompt content"""
76 | # question
77 | content = f'[?] {self.question}: ({self.keys})'
78 |
79 | # options, show only if it is expanded
80 | if self.is_expanded:
81 | for _, option in self.options.with_separators_enumerate():
82 |
83 | if type(option) is Separator:
84 | content += f"\n{option.label}"
85 | continue
86 |
87 | disabled = ("", "") if option.is_disabled else ("", "") # type: ignore
88 | # Disabled tags will only be inserted if the option is disabled,
89 | # otherwise there will only be an empty string inserted.
90 |
91 | if option.key == self.selection: # type: ignore
92 | content += f'\n{disabled[0]} {option.key}) {option.name}{disabled[1]}' # type: ignore
93 | else:
94 | content += f'\n{disabled[0]}{disabled[1]}' # type: ignore
95 |
96 | # text
97 | content += f'\n Answer: {self.selection} ({self.options.get_option(self.selection).name})' # type: ignore
98 |
99 | self.prompt_content.text = HTML(content)
100 |
101 | def prompt(self) -> str | None:
102 | """start the application, returns the return value"""
103 | self.update()
104 | out: str | None = self.run()
105 |
106 | return out
107 |
108 | def on_up(self):
109 | """when up is pressed, the previous indexed option will be selected"""
110 | if not self.allow_keyboard:
111 | return
112 |
113 | self.selection = self.keys[(self.keys.index(self.selection) - 1) % len(self.keys)]
114 |
115 | # if the current selection is disabled, we will skip it
116 | while self.options[(self.keys.index(self.selection)) % len(self.keys)].is_disabled:
117 | self.selection = self.keys[(self.keys.index(self.selection) - 1) % len(self.keys)]
118 |
119 | self.update()
120 |
121 | def on_down(self):
122 | """when down is pressed, the next indexed option will be selected"""
123 | if not self.allow_keyboard:
124 | return
125 |
126 | self.selection = self.keys[(self.keys.index(self.selection) + 1) % len(self.keys)]
127 |
128 | # if the current selection is disabled, we will skip it
129 | while self.options[(self.keys.index(self.selection)) % len(self.keys)].is_disabled:
130 | self.selection = self.keys[(self.keys.index(self.selection) + 1) % len(self.keys)]
131 |
132 | self.update()
133 |
134 | def on_key(self, key_sequence: list[str]):
135 | """when an index is pressed, which is available to select, select this index"""
136 | key = key_sequence[0]
137 |
138 | # return if key is not an available key
139 | if key not in self.keys:
140 | return
141 |
142 | # return if key is disabled
143 | if key in self.disabled:
144 | return
145 |
146 | # only expand if h is pressed, otherwise change selection
147 | if key == 'h':
148 | self.is_expanded = not self.is_expanded
149 | else:
150 | self.selection = key
151 |
152 | self.update()
153 |
154 | def on_enter(self):
155 | # if current selection is h-key, change is_expanded
156 | # otherwise return selected id
157 | if self.selection == 'h':
158 | self.is_expanded = not self.is_expanded
159 | self.update()
160 | return
161 |
162 | # get selected id
163 | self.exit(result=self.options[self.keys.index(self.selection)].id)
164 |
--------------------------------------------------------------------------------
/ItsPrompt/prompts/input.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from prompt_toolkit import Application, HTML
4 | from prompt_toolkit.buffer import Buffer
5 | from prompt_toolkit.completion import (
6 | Completer,
7 | FuzzyCompleter,
8 | FuzzyWordCompleter,
9 | NestedCompleter,
10 | )
11 | from prompt_toolkit.layout.containers import Window
12 | from prompt_toolkit.layout.controls import FormattedTextControl
13 | from prompt_toolkit.layout.processors import PasswordProcessor
14 |
15 | from ..objects.prompts.type import CompletionDict
16 |
17 |
18 | class InputPrompt(Application):
19 |
20 | def __init__(
21 | self,
22 | question: str,
23 | default: str | None = None,
24 | multiline: bool = False,
25 | show_symbol: str | None = None,
26 | validate: Callable[[str], str | bool | None] | None = None,
27 | completions: list[str] | CompletionDict | None = None,
28 | completer: Completer | None = None,
29 | *args,
30 | **kwargs,
31 | ):
32 | super().__init__(*args, **kwargs)
33 |
34 | # temp body variable for making object accessing clearer
35 | _body = self.layout.container.content.get_children() # type: ignore
36 | _vsplit = _body[0].get_children()
37 |
38 | # get prompt content box
39 | self.prompt_content: FormattedTextControl = _vsplit[0].content # type: ignore
40 |
41 | # get input buffer
42 | self.buffer: Buffer = _vsplit[1].content.buffer # type: ignore
43 |
44 | # save toolbar window and box (for showing errors)
45 | self.toolbar_window: Window = _body[1] # type: ignore
46 | self.toolbar_content: FormattedTextControl = self.toolbar_window.content # type: ignore
47 |
48 | # save standard toolbar content
49 | self.toolbar_content_default_text = self.toolbar_content.text # type: ignore
50 |
51 | # add updating function to buffer change event
52 | self.buffer.on_text_changed.add_handler(lambda _: self.update())
53 |
54 | # save whether error is currently shown
55 | self.is_error = False
56 |
57 | # save question
58 | self.question = question
59 |
60 | # save default input
61 | self.default = default
62 |
63 | # save whether multiline is enabled
64 | self.multiline = multiline
65 |
66 | # save show_symbol
67 | self.show_symbol = show_symbol
68 |
69 | # when show_symbol is given, use PasswordProcessor to show the symbol instead of the text
70 | if self.show_symbol:
71 | _vsplit[1].content.input_processors = [PasswordProcessor(self.show_symbol)]
72 |
73 | # save validator function
74 | self.validate = validate
75 |
76 | # save the completer
77 | self.completer: None | Completer = None
78 |
79 | if completions and completer:
80 | raise ValueError("completions and completer are mutually exclusive! Please use only one of them!")
81 |
82 | if show_symbol and (completions or completer):
83 | # symbol and completer are mutually exclusive
84 | raise ValueError('Completions are not compatible with show_symbol!')
85 |
86 | if completions: # pragma: no cover
87 | # a list or a dict of completions to use is given
88 | if type(completions) is list:
89 | # we use FuzzyWordCompleter
90 | self.completer = FuzzyWordCompleter(list(completions))
91 | elif type(completions) is dict:
92 | # we use FuzzyCompleter with NestedCompleter
93 | self.completer = FuzzyCompleter(NestedCompleter.from_nested_dict(completions))
94 |
95 | elif completer: # pragma: no cover
96 | # a self-created completer is given
97 | self.completer = completer
98 |
99 | # assign the created completer to the buffer
100 | if self.completer: # pragma: no cover
101 | self.buffer.completer = self.completer
102 |
103 | def update(self): # pragma: no cover
104 | """update prompt content"""
105 | content = f'[?] {self.question}: '
106 |
107 | if self.default and self.buffer.text == '':
108 | content += f'{self.default}'
109 |
110 | self.prompt_content.text = HTML(content)
111 |
112 | # run validation and show error, if validation gives error
113 | if not self.validate:
114 | return
115 |
116 | validation_result = self.validate(self.buffer.text)
117 |
118 | if validation_result in (None, True):
119 | self.is_error = False
120 | else:
121 | # validation_result is either a string (which will be the displayed error) or False (if the user used a
122 | # lambda statement)
123 | self.is_error = True
124 |
125 | # error is either the error returned by the validation function or a default error, if the validation
126 | # returned False (in case it is a lambda)
127 | error = validation_result if type(validation_result) is str else "Please check your Input!"
128 |
129 | # show error, if error should be shown, else show normal prompt
130 | if not self.is_error:
131 | # show normal prompt, change style to standard toolbar
132 | self.toolbar_content.text = self.toolbar_content_default_text
133 | self.toolbar_window.style = 'class:tooltip'
134 | else:
135 | # show error prompt and error style
136 | self.toolbar_content.text = error
137 | self.toolbar_window.style = 'class:error'
138 |
139 | # run completion
140 | # Since we take over control of the buffer and the keyboard, the completion
141 | # (and all of its commands) need to be run manually every time we press a key.
142 | # This does not in any way change the user experience,
143 | # as it does the exact same thing the standard completer does.
144 | if self.is_running:
145 | self.buffer.start_completion()
146 |
147 | def prompt(self) -> str | None:
148 | """start the application, returns the return value"""
149 | self.update()
150 | out: str | None = self.run()
151 |
152 | return out
153 |
154 | def _submit(self):
155 | """method for submitting result, as this is done by two functions"""
156 | # if an error is currently shown, prevent submit
157 | if self.is_error: # pragma: no cover
158 | return
159 |
160 | # return buffer if given, else default if given, else empty string
161 | if self.buffer.text != '':
162 | self.exit(result=self.buffer.text)
163 | elif self.default:
164 | self.exit(result=self.default)
165 | else:
166 | self.exit(result='')
167 |
168 | def on_alt_enter(self):
169 | """if multiline is enabled, this will submit"""
170 | if self.multiline:
171 | self._submit()
172 |
173 | def on_enter(self):
174 | """either submit key or in multiline, append new line"""
175 | # run completion
176 | if self.is_running and self.buffer.complete_state and (
177 | completion := self.buffer.complete_state.current_completion
178 | ): # pragma: no cover
179 | self.buffer.apply_completion(completion)
180 |
181 | if self.multiline:
182 | self.buffer.text += '\n'
183 | self.buffer.cursor_down()
184 | self.update()
185 | return
186 |
187 | self._submit()
188 |
--------------------------------------------------------------------------------
/ItsPrompt/prompts/raw_select.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit import Application, HTML
2 | from prompt_toolkit.layout.controls import FormattedTextControl
3 |
4 | from ..data.select import process_data
5 | from ..objects.prompts.separator import Separator
6 | from ..objects.prompts.type import OptionsList
7 |
8 |
9 | class RawSelectPrompt(Application):
10 |
11 | def __init__(
12 | self,
13 | question: str,
14 | options: OptionsList,
15 | default: str | None = None,
16 | disabled: tuple[str, ...] | None = None,
17 | allow_keyboard: bool = False,
18 | *args,
19 | **kwargs,
20 | ):
21 | super().__init__(*args, **kwargs)
22 |
23 | # get prompt content box
24 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore
25 |
26 | # save question
27 | self.question = question
28 |
29 | # self keyboard option
30 | self.allow_keyboard = allow_keyboard
31 |
32 | # process options
33 | self.options = process_data(options)
34 |
35 | # disable options (or none if not given)
36 | if disabled is not None:
37 | was_set = 0 # Keeping track of options which where disabled.
38 | # If was_set is not as big as len(disabled),
39 | # then we know that there was an invalid option which could not be disabled,
40 | # so we raise an error.
41 | for option in self.options:
42 | if option.id in disabled:
43 | option.is_disabled = True
44 | was_set += 1
45 |
46 | if was_set != len(disabled):
47 | raise ValueError("At least one of the given disabled values is invalid.")
48 |
49 | # default select default option (or first if not given)
50 | if default is not None:
51 | for i, option in enumerate(self.options):
52 | if option.id == default:
53 | if option.is_disabled:
54 | raise ValueError("Default value must not be disabled.")
55 | self.selection = i
56 | break
57 | else:
58 | raise ValueError('Default value is not a valid id.')
59 | else:
60 | self.selection = 0
61 |
62 | # if the current selection is disabled, we will skip it
63 | while self.options[self.selection].is_disabled:
64 | self.selection = (self.selection + 1) % len(self.options)
65 |
66 | def update(self):
67 | """update prompt content"""
68 | # question
69 | content = f'[?] {self.question}:'
70 |
71 | # options
72 | for i, option in self.options.with_separators_enumerate():
73 |
74 | if type(option) is Separator:
75 | content += f"\n{option.label}"
76 | continue
77 |
78 | disabled = ("", "") if option.is_disabled else ("", "") # type: ignore
79 | # Disabled tags will only be inserted if the option is disabled,
80 | # otherwise there will only be an empty string inserted.
81 |
82 | if i == self.selection:
83 | content += f'\n{disabled[0]} {i + 1}) {option.name}{disabled[1]}' # type: ignore
84 | else:
85 | content += f'\n{disabled[0]}{disabled[1]}' # type: ignore
86 |
87 | # text
88 | content += f'\n Answer: {self.selection + 1}'
89 |
90 | self.prompt_content.text = HTML(content)
91 |
92 | def prompt(self) -> str | None:
93 | """start the application, returns the return value"""
94 | self.update()
95 | out: str | None = self.run()
96 |
97 | return out
98 |
99 | def on_up(self):
100 | """when up is pressed, the previous indexed option will be selected"""
101 | if not self.allow_keyboard:
102 | return
103 |
104 | self.selection = (self.selection - 1) % len(self.options)
105 |
106 | # if the current selection is disabled, we will skip it
107 | while self.options[self.selection].is_disabled:
108 | self.selection = (self.selection - 1) % len(self.options)
109 |
110 | self.update()
111 |
112 | def on_down(self):
113 | """when down is pressed, the next indexed option will be selected"""
114 | if not self.allow_keyboard:
115 | return
116 |
117 | self.selection = (self.selection + 1) % len(self.options)
118 |
119 | # if the current selection is disabled, we will skip it
120 | while self.options[self.selection].is_disabled:
121 | self.selection = (self.selection + 1) % len(self.options)
122 |
123 | self.update()
124 |
125 | def on_key(self, key_sequence: list[str]):
126 | """when an index is pressed, which is available to select, select this index"""
127 | key = key_sequence[0]
128 |
129 | # return if key is not a number
130 | if not key.isnumeric():
131 | return
132 |
133 | id = int(key)
134 |
135 | # and return if key is not in the range of possible indices
136 | if id <= 0 or id > len(self.options):
137 | return
138 |
139 | # and return if selection is disabled
140 | if self.options[id - 1].is_disabled:
141 | return
142 |
143 | self.selection = id - 1
144 |
145 | self.update()
146 |
147 | def on_enter(self):
148 | # get selected id
149 | self.exit(result=self.options[self.selection].id)
150 |
--------------------------------------------------------------------------------
/ItsPrompt/prompts/select.py:
--------------------------------------------------------------------------------
1 | from prompt_toolkit import Application, HTML
2 | from prompt_toolkit.layout.controls import FormattedTextControl
3 |
4 | from ..data.select import process_data
5 | from ..objects.prompts.separator import Separator
6 | from ..objects.prompts.type import OptionsList
7 |
8 |
9 | class SelectPrompt(Application):
10 |
11 | def __init__(
12 | self,
13 | question: str,
14 | options: OptionsList,
15 | default: str | None = None,
16 | disabled: tuple[str, ...] | None = None,
17 | *args,
18 | **kwargs,
19 | ):
20 | super().__init__(*args, **kwargs)
21 |
22 | # get prompt content box
23 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore
24 |
25 | # save question
26 | self.question = question
27 |
28 | # process options
29 | self.options = process_data(options)
30 |
31 | # disable options
32 | if disabled is not None:
33 | was_set = 0 # Keeping track of options which where disabled.
34 | # If was_set is not as big as len(disabled),
35 | # then we know that there was an invalid option which could not be disabled,
36 | # so we raise an error.
37 | for option in self.options:
38 | if option.id in disabled:
39 | option.is_disabled = True
40 | was_set += 1
41 |
42 | if was_set != len(disabled):
43 | raise ValueError("At least one of the given disabled values is invalid.")
44 |
45 | # default select default option (or first if not given)
46 | if default is None:
47 | for i, option in enumerate(self.options):
48 | if not option.is_disabled:
49 | self.selection = i
50 | break
51 | else:
52 | for i, option in enumerate(self.options):
53 | if option.id == default:
54 | if option.is_disabled:
55 | raise ValueError("Default value must not be disabled.")
56 | self.selection = i
57 | break
58 | else:
59 | raise ValueError('Default value is not a valid id.')
60 |
61 | def update(self):
62 | """update prompt content"""
63 | content = f'[?] {self.question}:'
64 |
65 | for i, option in self.options.with_separators_enumerate():
66 |
67 | if type(option) is Separator:
68 | content += f"\n{option.label}"
69 | continue
70 |
71 | disabled = ("", "") if option.is_disabled else ("", "") # type: ignore
72 | # Disabled tags will only be inserted if the option is disabled,
73 | # otherwise there will only be an empty string inserted.
74 | if i == self.selection:
75 | content += \
76 | f'\n{disabled[0]} > {option.name}{disabled[1]}' # type: ignore
77 | else:
78 | content += \
79 | f'\n{disabled[0]}{disabled[1]}' # type: ignore
80 |
81 | self.prompt_content.text = HTML(content)
82 |
83 | def prompt(self) -> str | None:
84 | """start the application, returns the return value"""
85 | self.update()
86 | out: str | None = self.run()
87 |
88 | return out
89 |
90 | def on_up(self):
91 | """when up is pressed, the previous indexed option will be selected"""
92 | self.selection = (self.selection - 1) % len(self.options)
93 |
94 | # if the current selection is disabled, we will skip it
95 | while self.options[self.selection].is_disabled:
96 | self.selection = (self.selection - 1) % len(self.options)
97 |
98 | self.update()
99 |
100 | def on_down(self):
101 | """when down is pressed, the next indexed option will be selected"""
102 | self.selection = (self.selection + 1) % len(self.options)
103 |
104 | # if the current selection is disabled, we will skip it
105 | while self.options[self.selection].is_disabled:
106 | self.selection = (self.selection + 1) % len(self.options)
107 |
108 | self.update()
109 |
110 | def on_enter(self):
111 | # get selected id
112 | self.exit(result=self.options[self.selection].id)
113 |
--------------------------------------------------------------------------------
/ItsPrompt/prompts/table.py:
--------------------------------------------------------------------------------
1 | import html
2 | from typing import TYPE_CHECKING, Union
3 |
4 | from prompt_toolkit import Application, HTML
5 | from prompt_toolkit.data_structures import Point
6 | from prompt_toolkit.layout.controls import FormattedTextControl
7 |
8 | from ..data.table import Table
9 | from ..objects.prompts.type import TablePromptDict, TablePromptList
10 |
11 | if TYPE_CHECKING: # pragma: no cover
12 | from pandas import DataFrame
13 |
14 |
15 | class TablePrompt(Application):
16 |
17 | def __init__(
18 | self,
19 | question: str,
20 | data: Union["DataFrame", TablePromptDict, TablePromptList],
21 | *args,
22 | **kwargs,
23 | ):
24 | super().__init__(*args, **kwargs)
25 |
26 | # get prompt content box
27 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore
28 |
29 | # save question
30 | self.question = question
31 |
32 | # process data
33 | self.table = Table(data)
34 |
35 | # set cursor
36 | self.prompt_content.get_cursor_position = lambda: Point(
37 | self.table.get_current_cursor_position()[0],
38 | self.table.get_current_cursor_position()[1] + 1, # add 1 offset for height of question
39 | )
40 |
41 | def update(self):
42 | """update prompt content"""
43 | content = f'[?] {self.question}:\n'
44 |
45 | # append table
46 | content += html.escape(
47 | self.table.get_table_as_str()
48 | ) # escaping is needed so formatting from PromptToolkit won't destroy the whole table
49 |
50 | self.prompt_content.text = HTML(content)
51 |
52 | def prompt(self) -> Union["DataFrame", TablePromptDict, None]:
53 | """start the application, returns the return value"""
54 | self.update()
55 | out: Union["DataFrame", TablePromptDict, None] = self.run()
56 |
57 | return out
58 |
59 | def on_up(self):
60 | """when up is pressed, the cell one above will be selected"""
61 | self.table.on_up()
62 |
63 | self.update()
64 |
65 | def on_down(self):
66 | """when down is pressed, the cell one below will be selected"""
67 | self.table.on_down()
68 |
69 | self.update()
70 |
71 | def on_left(self):
72 | """when left is pressed, the cell one to the left will be selected"""
73 | self.table.on_left()
74 |
75 | self.update()
76 |
77 | def on_right(self):
78 | """when left is pressed, the cell one to the right will be selected"""
79 | self.table.on_right()
80 |
81 | self.update()
82 |
83 | def on_key(self, key_sequence: list[str]):
84 | """when a key is pressed, the key will be added to the current cells content"""
85 | key = key_sequence[0]
86 | self.table.add_key(key)
87 |
88 | self.update()
89 |
90 | def on_backspace(self):
91 | """when backspace is pressed, the last char will be removed from the current cells content"""
92 | self.table.del_key()
93 |
94 | self.update()
95 |
96 | def on_enter(self):
97 | # return the modified table
98 | self.exit(result=self.table.data.get_data())
99 |
--------------------------------------------------------------------------------
/ItsPrompt/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/ItsPrompt/py.typed
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023-present ItsNameless
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://badge.fury.io/py/ItsPrompt)
2 | [](https://github.com/TheItsProjects/ItsPrompt/actions/workflows/lint.yml)
3 | [](https://github.com/TheItsProjects/ItsPrompt/actions/workflows/tests.yml)
4 |
5 | [](https://pypi.org/project/ItsPrompt/)
6 | [](https://github.com/TheItsProjects/ItsPrompt/issues)
7 | [](https://github.com/TheItsProjects/ItsPrompt/stargazers)
8 | [](https://github.com/TheItsProjects/ItsPrompt/blob/main/LICENSE)
9 | [](https://discord.gg/rP9Qke2jDs)
10 |
11 | [](http://itsprompt.readthedocs.io/)
12 |
13 | 
14 |
15 | # ItsPrompt
16 |
17 | Do you ever feel the need to ask a user of your code for an input?
18 |
19 | Using `input()` is easy, but is it great?
20 |
21 | Do you want to give the user a selection list, a yes-or-no question, or maybe a multiline input field?
22 |
23 | And do you think all of this should be done easily, without caring to much how it all works?
24 |
25 | Then you are right here! **ItsPrompt** allows you to ask the user for input, the *fancy* way.
26 |
27 | **ItsPrompt** tries to be an easy-to-use module for managing prompts for the user. Your task is to create a great
28 | program, not finding yourself questioning how to ask the user for input. That is why **ItsPrompt** is there to take care
29 | of this problem, so you can focus on the important things!
30 |
31 | ## TOC
32 |
33 |
34 | * [ItsPrompt](#itsprompt)
35 | * [TOC](#toc)
36 | * [A small, thankful note](#a-small-thankful-note)
37 | * [Features](#features)
38 | * [Installation](#installation)
39 | * [Quick Example](#quick-example)
40 | * [Usage](#usage)
41 | * [Further Information](#further-information)
42 |
43 |
44 | ---
45 |
46 | ### A small, thankful note
47 |
48 | This project is not the first to accomplish the above-mentioned tasks. There is another package, `PyInquirer`, which
49 | inspired me to build **ItsPrompt**.
50 |
51 | On my way to create a small program I came to a point were I needed a simple GUI, and I tried `PyInquirer`.
52 | Unfortunately, at the current time it is not actively maintained and a bit outdated. I thought of updating it, but then
53 | I thought "*Isn't it easier to just create my own version?*" - And so I did!
54 |
55 | **ItsPrompt** is not a copy or a fork of `PyInquirer`. I built this module from the ground up, without ever looking deep
56 | into the source code of `PyInquirer`.
57 |
58 | On my way to build this package, I learned a lot about `prompt-toolkit`, and all of this just because of `PyInquirer`!
59 | Thanks!
60 |
61 | ---
62 |
63 | ## Features
64 |
65 | - many prompt types:
66 | - select
67 | - raw_select
68 | - expand
69 | - checkbox
70 | - confirm
71 | - input
72 | - table
73 | - prompt autocompletion and validation
74 | - customizable style with `prompt_toolkit`
75 | - a helpful toolbar with error messages
76 | - simple, pythonic syntax
77 |
78 | ---
79 |
80 | ## Installation
81 |
82 | This package is hosted on pypi, so the installation is as simple as it can get:
83 |
84 | ```bash
85 | python3 -m pip install ItsPrompt
86 | ```
87 |
88 | This will install `ItsPrompt` without pandas. If you want to use `TablePrompt`
89 | (see [table](https://itsprompt.readthedocs.io/en/latest/guide/prompt_types.html#table)) with
90 | `pandas.DataFrame`, you can install pandas support either by:
91 |
92 | - installing pandas separately
93 | - install `ItsPrompt` via `pip install ItsPrompt[df]`
94 |
95 | ---
96 |
97 | ## Quick Example
98 |
99 | Import the `Prompt` class:
100 |
101 | ```py
102 | from ItsPrompt.prompt import Prompt
103 | ```
104 |
105 | Now you can ask the user any type of prompt by calling the specific function from the `Prompt` class, e.g.:
106 |
107 | ```py
108 | result = Prompt.input('What is your name?')
109 | print(result)
110 | ```
111 |
112 | You see how easy it is?
113 |
114 | ---
115 |
116 | ## Usage
117 |
118 | To learn more about the usage, visit our [documentation](https://itsprompt.readthedocs.io).
119 |
120 | ---
121 |
122 | ## Further Information
123 |
124 | Visit our [documentation](https://itsprompt.readthedocs.io/) to learn more about the usage of **ItsPrompt**!
125 |
126 | If you need some easy examples, refer to [example.py](example.py)!
127 |
128 | If you want to contribute, check out the projects repository: [ItsPrompt](https://github.com/TheItsProjects/ItsPrompt)!
129 |
130 | If you got any other questions, or want to give an idea on how to improve **ItsPrompt**:
131 |
132 | - visit our discussions: [ItsPrompt Discussions](https://github.com/TheItsProjects/ItsPrompt/discussions)!
133 | - join our discord: [TheItsProjects](https://discord.gg/rP9Qke2jDs)!
134 |
135 | ---
136 |
137 | Puh, that was so much to read... But now, lets have fun with **ItsPrompt**!
138 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | This page helps you find out how and when to report security vulnerabilites you encoutered.
4 |
5 | ## Supported Versions
6 |
7 | Currently we do not have different versions of `ItsPrompt`, so all of the minor versions of `1.x` are supported.
8 |
9 | | Version | Supported |
10 | |---------|--------------------|
11 | | 1.x | :white_check_mark: |
12 |
13 | ## Reporting a Vulnerability
14 |
15 | You found a security vulnerability?
16 |
17 | No problem! Open an Issue over at [select an Issue type](https://github.com/TheItsProjects/ItsPrompt/issues/new/choose)
18 | and choose **Security Vulnerability**. Fill in the required information and then we will look into it!
19 |
20 | If you want to privately report an Issue, visit our [Discord Server](https://discord.gg/rP9Qke2jDs) for more info!
21 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | # test command
16 | vhs:
17 | @echo "Creating VHS Media"
18 | cd scripts/vhs && ./generate.sh
19 |
20 |
21 | .PHONY: help Makefile vhs
22 |
23 | # Catch-all target: route all unknown targets to Sphinx using the new
24 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
25 | %: Makefile
26 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
27 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx==7.2.6
2 | sphinx-rtd-theme==2.0.0
3 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/checkbox.tape:
--------------------------------------------------------------------------------
1 | # --- Set Up --------------------------
2 | # Load Config
3 | Source ./configuration/base_config.tape
4 | Source ./configuration/small_window_config.tape
5 |
6 | # --- Main ----------------------------
7 | # run example.py
8 | Type@0.1s "python3 demos/checkbox_demo.py" Enter Sleep 1s
9 |
10 | Screenshot ../../source/media/checkbox.png
11 |
12 | Sleep 1s
13 |
14 | Ctrl+C
15 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/configuration/base_config.tape:
--------------------------------------------------------------------------------
1 | # --- Settings ------------------------
2 | # Font
3 | Set FontFamily "Cascadia Mono"
4 |
5 | # Behaviour
6 | Set TypingSpeed 0.5s
7 | Set Shell "bash"
8 |
9 | # --- Set Up Prompt -------------------
10 | Hide
11 | Type@10ms ". ./configuration/config.sh" Enter
12 | Sleep 5s
13 | Show
14 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/configuration/big_window_config.tape:
--------------------------------------------------------------------------------
1 | # Configuration for big window tapes, like the demo
2 | # These settings include a big window with gradient background, a colorful window bar, and a big font size
3 |
4 | # --- Settings ------------------------
5 | # Font
6 | Set FontSize 20
7 |
8 | # Terminal Size
9 | Set Width 1200
10 | Set Height 600
11 |
12 | # Window
13 | Set WindowBar "Colorful"
14 | Set WindowBarSize 80
15 | Set Padding 0
16 |
17 | # Background
18 | Set Margin 25
19 | Set MarginFill ./configuration/gradient.png
20 |
21 | # Window
22 | Set BorderRadius 15
23 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/configuration/config.sh:
--------------------------------------------------------------------------------
1 | # Configure path and venv
2 | cd ../../../
3 | source venv/bin/activate
4 |
5 | # Install Package
6 | pip install -e .
7 | cd examples/ || exit
8 |
9 | # Configure Prompt Style
10 | CYAN='\[\e[36m\]'
11 | YELLOW='\[\e[33m\]'
12 | BOLD='\[\e[1m\]'
13 | RESET='\[\e[0m\]'
14 |
15 | export PS1="${CYAN}${BOLD}user@itsprompt ${YELLOW}${BOLD}> ${RESET}"
16 |
17 | # Clear the terminal
18 | clear
19 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/configuration/gradient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/scripts/vhs/configuration/gradient.png
--------------------------------------------------------------------------------
/docs/scripts/vhs/configuration/small_window_config.tape:
--------------------------------------------------------------------------------
1 | # Configuration for small window tapes, like the ones used in prompt types
2 | # These settings include a small window without margin and background, no window bar, and a small font size
3 |
4 | # --- Settings ------------------------
5 | # Font
6 | Set FontSize 25
7 |
8 | # Terminal Size
9 | Set Width 900
10 | Set Height 400
11 |
12 | # Window
13 | Set WindowBarSize 0
14 | Set Padding 0
15 |
16 | # Background
17 | Set Margin 0
18 |
19 | # Window
20 | # Set BorderRadius 0
21 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/confirm.tape:
--------------------------------------------------------------------------------
1 | # --- Set Up --------------------------
2 | # Load Config
3 | Source ./configuration/base_config.tape
4 | Source ./configuration/small_window_config.tape
5 |
6 | # --- Main ----------------------------
7 | # run example.py
8 | Type@0.1s "python3 demos/confirm_demo.py" Enter Sleep 1s
9 |
10 | Screenshot ../../source/media/confirm.png
11 |
12 | Sleep 1s
13 |
14 | Ctrl+C
15 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/demo.tape:
--------------------------------------------------------------------------------
1 | # --- Set Up --------------------------
2 | # Load Config
3 | Source ./configuration/base_config.tape
4 | Source ./configuration/big_window_config.tape
5 |
6 | # Set Output
7 | Output ../../source/media/ItsPrompt.gif
8 |
9 | # --- Main ----------------------------
10 | # run example.py
11 | Type@0.1s "python3 demo.py" Enter Sleep 1s
12 |
13 | # Prompt 1
14 | Up
15 | Down 2
16 | Enter
17 | Sleep 1s
18 |
19 | # Prompt 2
20 | Up 2
21 | Enter
22 | Sleep 1s
23 |
24 | # Prompt 3
25 | Space
26 | Enter
27 | Down
28 | Space
29 | Enter
30 | Sleep 1s
31 |
32 | # Prompt 4
33 | Type "h"
34 | Type "m"
35 | Enter
36 | Sleep 1s
37 |
38 | # Prompt 5
39 | Type@0.1s "test" Enter
40 | Sleep 1s
41 |
42 | # Prompt 6
43 | Type@0.1s "ItsNameless" Enter
44 | Sleep 1s
45 |
46 | # Prompt 7
47 | Type@0.1s "Main"
48 | Tab
49 | Enter
50 | Sleep 1s
51 |
52 | # Prompt 8
53 | Type@0.1s "1234" Enter
54 | Sleep 1s
55 |
56 | # Prompt 9
57 | Type "y"
58 | Sleep 1s
59 |
60 | # Prompt 10
61 | Right
62 | Down
63 | Backspace
64 | Type "1" Enter
65 | Sleep 1s
66 |
67 | # Exit
68 | Hide
69 | Ctrl+C
70 | Show
71 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/expand.tape:
--------------------------------------------------------------------------------
1 | # --- Set Up --------------------------
2 | # Load Config
3 | Source ./configuration/base_config.tape
4 | Source ./configuration/small_window_config.tape
5 |
6 | # --- Main ----------------------------
7 | # run example.py
8 | Type@0.1s "python3 demos/expand_demo.py" Enter Sleep 1s
9 |
10 | Type "h" Sleep 1s
11 |
12 | Screenshot ../../source/media/expand.png
13 |
14 | Sleep 1s
15 |
16 | Ctrl+C
17 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/generate.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Run this file to create tapes for all .tape files in the directory.
4 | # The tapes will be saved to source/media.
5 | # To run this file, you need to have vhs installed. Read more in the development guide.
6 |
7 | trap "echo 'Script interrupted by user'; exit 1" SIGINT SIGTERM
8 |
9 | # Get the directory of the script
10 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
11 |
12 | # Iterate over all .tape files in the directory
13 | for file in "$DIR"/*.tape
14 | do
15 | # Run the file with vhs
16 | echo "--- Creating media for $file ---"
17 | vhs "$file" 1> /dev/null
18 | done
19 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/input.tape:
--------------------------------------------------------------------------------
1 | # --- Set Up --------------------------
2 | # Load Config
3 | Source ./configuration/base_config.tape
4 | Source ./configuration/small_window_config.tape
5 |
6 | # --- Main ----------------------------
7 | # run example.py
8 | Type@0.1s "python3 demos/input_demo.py" Enter Sleep 1s
9 |
10 | Screenshot ../../source/media/input.png
11 |
12 | Sleep 1s
13 |
14 | Ctrl+C
15 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/raw_select.tape:
--------------------------------------------------------------------------------
1 | # --- Set Up --------------------------
2 | # Load Config
3 | Source ./configuration/base_config.tape
4 | Source ./configuration/small_window_config.tape
5 |
6 | # --- Main ----------------------------
7 | # run example.py
8 | Type@0.1s "python3 demos/raw_select_demo.py" Enter Sleep 1s
9 |
10 | Screenshot ../../source/media/raw_select.png
11 |
12 | Sleep 1s
13 |
14 | Ctrl+C
15 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/select.tape:
--------------------------------------------------------------------------------
1 | # --- Set Up --------------------------
2 | # Load Config
3 | Source ./configuration/base_config.tape
4 | Source ./configuration/small_window_config.tape
5 |
6 | # --- Main ----------------------------
7 | # run example.py
8 | Type@0.1s "python3 demos/select_demo.py" Enter Sleep 1s
9 |
10 | Screenshot ../../source/media/select.png
11 |
12 | Sleep 1s
13 |
14 | Ctrl+C
15 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/table.tape:
--------------------------------------------------------------------------------
1 | # --- Set Up --------------------------
2 | # Load Config
3 | Source ./configuration/base_config.tape
4 | Source ./configuration/small_window_config.tape
5 |
6 | # --- Main ----------------------------
7 | # run example.py
8 | Type@0.1s "python3 demos/table_demo.py" Enter Sleep 1s
9 |
10 | Screenshot ../../source/media/table.png
11 |
12 | Sleep 1s
13 |
14 | Ctrl+C
15 |
--------------------------------------------------------------------------------
/docs/scripts/vhs/tape.template:
--------------------------------------------------------------------------------
1 | # --- Set Up --------------------------
2 | # Load Config
3 | Source ./configuration/base_config.tape
4 | Source ./configuration/small_window_config.tape # use big_window_config.tape for bigger window
5 |
6 | # --- Main ----------------------------
7 | # run example.py
8 | Type@0.1s "python3 demos/FILE.py" Enter Sleep 1s # replace FILE.py with the file you want to run
9 |
10 | Screenshot ../../source/media/MEDIA.png # replace MEDIA.png with the name of the file you want to save the screenshot to
11 |
12 | Sleep 1s
13 |
14 | Ctrl+C
15 |
--------------------------------------------------------------------------------
/docs/source/api/objects.rst:
--------------------------------------------------------------------------------
1 | Objects API Reference
2 | =====================
3 |
4 | This is the API reference for the ``objects`` used for the :doc:`prompt` methods.
5 |
6 | Prompt Input Objects
7 | --------------------
8 |
9 | .. automodule:: ItsPrompt.objects.prompts.type
10 | :members:
11 | :undoc-members:
12 |
13 | .. autoclass:: ItsPrompt.objects.prompts.separator.Separator
14 | :members:
15 | :undoc-members:
16 |
17 | Styling Objects
18 | ---------------
19 |
20 | .. automodule:: ItsPrompt.data.style
21 | :members:
22 | :exclude-members: convert_style
23 | :undoc-members:
24 |
--------------------------------------------------------------------------------
/docs/source/api/prompt.rst:
--------------------------------------------------------------------------------
1 | Prompt API Reference
2 | ====================
3 |
4 | This is the API reference for the `Prompt` class. The `Prompt` class is the main class of the `ItsPrompt` package. It is used to create and display prompts to the user.
5 |
6 | To use any of the given methods, you must first import the ``Prompt`` class from the `ItsPrompt` package. Here is an example of how to import the ``Prompt`` class:
7 |
8 | .. code-block:: python
9 |
10 | from ItsPrompt.prompt import Prompt
11 |
12 | .. currentmodule:: ItsPrompt.prompt
13 |
14 | .. automethod:: Prompt.select
15 |
16 | .. automethod:: Prompt.raw_select
17 |
18 | .. automethod:: Prompt.expand
19 |
20 | .. automethod:: Prompt.checkbox
21 |
22 | .. automethod:: Prompt.confirm
23 |
24 | .. automethod:: Prompt.input
25 |
26 | .. automethod:: Prompt.table
27 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 |
9 | project = 'ItsPrompt'
10 | copyright = '2024-present, ItsNameless'
11 | author = 'ItsNameless'
12 | version = '1.5'
13 |
14 | # -- General configuration ---------------------------------------------------
15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
16 |
17 | extensions = [
18 | 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', 'sphinx.ext.doctest', 'sphinx.ext.coverage'
19 | ]
20 |
21 | intersphinx_mapping = {
22 | 'python': ('https://docs.python.org/3', None),
23 | 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None),
24 | 'prompt_toolkit': ('https://python-prompt-toolkit.readthedocs.io/en/master/', None),
25 | }
26 | templates_path = ['_templates']
27 | exclude_patterns = [] # type: ignore
28 |
29 | # -- Options for HTML output -------------------------------------------------
30 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
31 |
32 | html_theme = 'sphinx_rtd_theme'
33 | html_static_path = ['_static']
34 |
--------------------------------------------------------------------------------
/docs/source/development_guide/documentation.rst:
--------------------------------------------------------------------------------
1 | Writing Documentation for ItsPrompt
2 | ===================================
3 |
4 | This document describes how to write documentation for ItsPrompt.
5 |
6 | Basics of ItsPrompt Documentation
7 | ---------------------------------
8 |
9 | `ItsPrompt` uses `Sphinx` for documentation. The documentation is written in `reStructuredText` format. The source
10 | can be found in the `docs/source` directory.
11 |
12 | Creating Media Files using vhs
13 | ------------------------------
14 |
15 | `ItsPrompt` uses `vhs `_ to record terminal sessions and create gif and image files
16 | for the documentation. Before running any command, refer to the `vhs` documentation to understand how to install and use it.
17 |
18 | If you simply want to update all the included media, you can run the following command:
19 |
20 | .. code-block:: bash
21 |
22 | make vhs
23 |
24 | The generated media files will be placed in the `docs/source/media` directory.
25 |
26 | Adding Media Files to the Documentation
27 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
28 |
29 | To add a new media file to the documentation, you need to create a new `.tape` file in the `docs/scripts/vhs` directory.
30 |
31 | You can use the content from `tape.template` for a quick guide on how to create a new `.tape` file.
32 |
--------------------------------------------------------------------------------
/docs/source/development_guide/getting_started.rst:
--------------------------------------------------------------------------------
1 | ItsPrompt Development Guide
2 | ===========================
3 |
4 | This guide is intended to help developers get started with the Development for ItsPrompt.
5 |
--------------------------------------------------------------------------------
/docs/source/guide/getting_started.rst:
--------------------------------------------------------------------------------
1 | Getting Started with ItsPrompt
2 | ==============================
3 |
4 | What is ItsPrompt?
5 | ------------------
6 |
7 | **ItsPrompt** is a Python package that simplifies user interaction in command-line interfaces. It offers a variety of
8 | prompt types, including:
9 |
10 | - `select`
11 | - `raw_select`
12 | - `expand`
13 | - `checkbox`
14 | - `confirm`
15 | - `input`
16 | - `table`
17 |
18 | Each prompt type provides a unique way of gathering user input. The package is designed with simplicity in mind,
19 | offering a straightforward, Pythonic syntax. It also provides a range of customization options, allowing you to style
20 | prompts and validate user input.
21 |
22 | ItsPrompt is built on top of the `prompt-toolkit` library, leveraging its capabilities for prompt creation and styling.
23 | This makes it an excellent tool for developers who want to create interactive command-line applications without having
24 | to worry about the intricacies of user input and command-line rendering.
25 |
26 | Installation
27 | ------------
28 |
29 | This package is hosted on pypi, so the installation is as simple as it can get:
30 |
31 | .. code-block:: bash
32 |
33 | python3 -m pip install ItsPrompt
34 |
35 |
36 | This will install `ItsPrompt` without pandas. If you want to use `TablePrompt` (see :ref:`prompt_types_table`) with
37 | `pandas.DataFrame`, you can install pandas support either by:
38 |
39 | - installing pandas separately
40 | - install `ItsPrompt` via ``pip install ItsPrompt[df]``
41 |
42 | Basic Usage
43 | -----------
44 |
45 | Import the `Prompt` class:
46 |
47 | .. code-block:: python
48 |
49 | >>> from ItsPrompt.prompt import Prompt
50 |
51 | Now you can ask the user any type of prompt by calling the specific function from the `Prompt` class, e.g.:
52 |
53 | .. code-block:: python
54 |
55 | >>> result = Prompt.input('What is your name?')
56 | [?] What is your name?: ItsNameless
57 | >>> print(f"Hello {result}!")
58 | Hello ItsNameless!
59 |
60 | You see how easy it is?
61 |
62 | To learn more about the usage of ItsPrompt, check out :doc:`usage`.
63 |
--------------------------------------------------------------------------------
/docs/source/guide/options_and_data.rst:
--------------------------------------------------------------------------------
1 | Options and Data in ItsPrompt
2 | =============================
3 |
4 | `ItsPrompt` has a number of ways to display and check options and data.
5 |
6 | Option Parameter
7 | ----------------
8 |
9 | The `option` parameter is always a :class:`tuple` with the type annotation :const:`~ItsPrompt.objects.prompts.type.OptionsList`.
10 |
11 | Option as a String
12 | ~~~~~~~~~~~~~~~~~~
13 |
14 | Options can be given as a :class:`str`. In this case, the string is displayed as the option and returned as the
15 | selected option, if the user selects it.
16 |
17 | .. note:: In case of :meth:`~ItsPrompt.prompt.Prompt.expand()`, the first character of the :class:`str` will be used as the key.
18 |
19 | Options as a Tuple of OptionWithId
20 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
21 |
22 | Options can be given as a :class:`tuple` with the type annotation :const:`~ItsPrompt.objects.prompts.type.OptionWithId`.
23 |
24 | For all prompts excluding :meth:`~ItsPrompt.prompt.Prompt.expand`, the tuple is structured as follows
25 |
26 | - The first element is the displayed option
27 | - The second element is the id of the option
28 |
29 | For the :meth:`~ItsPrompt.prompt.Prompt.expand` prompt, the tuple is structured as follows:
30 |
31 | - The first element is the key of the option
32 | - The second element is the displayed option
33 | - The third element is the id of the option
34 |
35 | Separators
36 | ~~~~~~~~~~
37 |
38 | Options can be separated using a :class:`~ItsPrompt.objects.prompts.type.Separator`. A Separator name can be given using
39 | the `label` parameter.
40 |
41 | Separators are available in the prompt types:
42 |
43 | - :meth:`~ItsPrompt.prompt.Prompt.select`
44 | - :meth:`~ItsPrompt.prompt.Prompt.raw_select`
45 | - :meth:`~ItsPrompt.prompt.Prompt.checkbox`
46 | - :meth:`~ItsPrompt.prompt.Prompt.expand`
47 |
48 | .. note:: The separator cannot be selected as an option. It is purely for cosmetic purposes.
49 |
50 | Example:
51 |
52 | .. code-block:: python
53 |
54 | from ItsPrompt.prompt import Prompt
55 | from ItsPrompt.objects.prompts.separator import Separator
56 |
57 | ans = Prompt.select(
58 | 'What food would you like?',
59 | (Separator('The veggies'), 'Salad', Separator('The meaties'), 'Pizza', 'Burger'),
60 | default='Pizza',
61 | )
62 |
63 | Data Parameter
64 | --------------
65 |
66 | The :meth:`~ItsPrompt.prompt.Prompt.table` prompt has a `data` parameter instead of the `option` parameter. The `data`
67 | parameter has to be one of the following types:
68 |
69 | - a :class:`~ItsPrompt.objects.prompts.type.TablePromptList`
70 | - a :class:`~ItsPrompt.objects.prompts.type.TablePromptDict`
71 | - a :class:`pandas.DataFrame`
72 |
73 | .. note:: The type :class:`pandas.DataFrame` is only available if the `pandas` library is installed.
74 |
75 | The `data` is used as the content of the table. The user may change the fields of the table. The output of the `table`
76 | prompt is of the same type, as the input data is represented, with the user given values.
77 |
78 | .. note::
79 |
80 | Currently, all fields are represented as strings and every field is editable by the user. This may be changed
81 | in the future.
82 |
83 | Data as a TablePromptList
84 | ~~~~~~~~~~~~~~~~~~~~~~~~~
85 |
86 | A :class:`~ItsPrompt.objects.prompts.type.TablePromptList` is a :class:`list` of :class:`list` with each cell
87 | represented by a :class:`str`.
88 |
89 | Every sub-list represents a column in the table.
90 |
91 | .. code-block:: python
92 |
93 | data = [
94 | ["field 1", "field 2"],
95 | ["field 3", "field 4"]
96 | ]
97 |
98 | will be rendered:
99 |
100 | +---------+---------+
101 | | 0 | 1 |
102 | +=========+=========+
103 | | field 1 | field 3 |
104 | +---------+---------+
105 | | field 2 | field 4 |
106 | +---------+---------+
107 |
108 | Data as a TablePromptDict
109 | ~~~~~~~~~~~~~~~~~~~~~~~~~
110 |
111 | A :class:`~ItsPrompt.objects.prompts.type.TablePromptDict` is a :class:`dict` with the keys as the column names and the
112 | values as a :class:`list` of :class:`str`, where each :class:`list` represents a column in the table.
113 |
114 | .. code-block:: python
115 |
116 | data = {
117 | "column 1": ["field 1", "field 2"],
118 | "column 2": ["field 3", "field 4"]
119 | }
120 |
121 | +----------+----------+
122 | | column 1 | column 2 |
123 | +==========+==========+
124 | | field 1 | field 3 |
125 | +----------+----------+
126 | | field 2 | field 4 |
127 | +----------+----------+
128 |
129 | Data as a DataFrame
130 | ~~~~~~~~~~~~~~~~~~~
131 |
132 | A :class:`pandas.DataFrame` can be used as well. Read more about them in the `pandas documentation `_.
133 |
134 | .. code-block:: python
135 |
136 | DataFrame(["field 1", "field 2"])
137 |
138 | +---------+
139 | | 0 |
140 | +=========+
141 | | field 1 |
142 | +---------+
143 | | field 2 |
144 | +---------+
145 |
146 | .. note::
147 |
148 | Currently, the `table` prompt cannot display styling in the `DataFrame` fields. All styling tags will be displayed
149 | as-is, so a `...` will not be underlined, but rather displayed as its shown.
150 |
151 | Input Prompt Parameters
152 | -----------------------
153 |
154 | The :meth:`~ItsPrompt.prompt.Prompt.input` prompt has many options to check and complete the input. The following parameters are available:
155 |
156 | - `default`: The default value of the input. If the user does not enter anything, the default value is returned.
157 | - `multiline`: If set to :const:`True`, the user can enter multiple lines of text.
158 | - `show_symbol`: Can be set to a :class:`str` to show this symbol instead of the characters entered by the user.
159 | This is useful for password prompts.
160 | - `validate`: A function that validates the input. Read more about this in the :ref:`validation` section.
161 | - `completions`: A list of completions that the user can select from. Read more about this in the :ref:`completions` section.
162 | - `completer`: A function that returns a list of completions. Read more about this in the :ref:`completions` section.
163 |
164 | .. note::
165 |
166 | Only the `validate`, `completions`, and `completer` parameters are described here in detail. The other parameters can
167 | be found in the :meth:`~ItsPrompt.prompt.Prompt.input` API documentation.
168 |
169 | .. _validation:
170 |
171 | Prompt Validation
172 | ~~~~~~~~~~~~~~~~~
173 |
174 | The `validate` parameter can be used to validate the input. For every character entered by the user, the `validate`
175 | function is called with the current input as a :class:`str`. The function should return either
176 |
177 | - a :class:`str` to show the string as an error message
178 | - :const:`False` to show a default error message
179 | - :const:`None` (or :const:`True`) to accept the input
180 |
181 | .. code-block:: python
182 |
183 | # define a validation function
184 | def input_not_empty(input: str) -> str | None:
185 | if len(input) == 0:
186 | return 'Address can not be empty!'
187 |
188 |
189 | # using a function
190 | Prompt.input(
191 | ...,
192 | validate=input_not_empty,
193 | ...,
194 | )
195 |
196 | # using lambda
197 | Prompt.input(
198 | ...,
199 | validate=lambda x: "test" in x,
200 | ...,
201 | )
202 |
203 | The :class:`str` given to the function is the current input of the user. It cannot be changed.
204 |
205 | If you want to show that the validation succeeded, return :const:`None` (or nothing, or :const:`True`). This will not
206 | trigger any errors.
207 |
208 | If you want to show an error, return a :class:`str` with the errors text or :const:`False`. If you return a
209 | :class:`str`, your text will be shown in the toolbar. If you return :const:`False`, a general error message will be
210 | shown. As long as the validation returns a :class:`str` or :const:`False`, the user may not submit the input.
211 |
212 | .. _completions:
213 |
214 | Prompt Completion
215 | ~~~~~~~~~~~~~~~~~
216 |
217 | The `completions` and `completer` parameters can be used to give the user a list of completions to choose from.
218 |
219 | .. note:: If you use `completions` or `completer`, you are unable to use `show_symbol`.
220 |
221 | .. note:: `completions` and `completer` are **mutually exclusive**!. You may only use one of them at a time.
222 |
223 | There are three ways to give completions to the user:
224 |
225 | - A :class:`list` of :class:`str` (read more about this in the :ref:`completions_as_a_list` section)
226 | - A nested :class:`dict` (read more about this in the :ref:`completions_as_a_dict` section)
227 | - A :class:`~prompt_toolkit.completion.Completer` given by `prompt_toolkit` (read more about this in the
228 | :ref:`completions_as_a_completer` section)
229 |
230 | .. _completions_as_a_list:
231 |
232 | Completions as a List
233 | *********************
234 |
235 | :meth:`~ItsPrompt.prompt.Prompt.input` takes a :class:`list[str]` to use as simple word completions. Each :class:`str` in the list is a possible value to complete.
236 |
237 | .. code-block:: python
238 |
239 | prompt.input(
240 | ...,
241 | completions=['Mainstreet 4', 'Fifth way'],
242 | ...,
243 | )
244 |
245 | .. _completions_as_a_dict:
246 |
247 | Completions as a Dict
248 | *********************
249 |
250 | You can use a :class:`dict` for nested completions. Each "layer" will be a completion, after the first was accepted. The
251 | type annotation for the :class:`dict` can be found here: :const:`~ItsPrompt.objects.prompts.type.CompletionDict`.
252 |
253 | Example:
254 |
255 | .. code-block:: python
256 |
257 | completions = {
258 | '1': {
259 | '1.1': None,
260 | '1.2': {
261 | '1.2.1', '1.2.2'
262 | }
263 | },
264 | '2': {
265 | '2.1': {'2.1.1'}
266 | }
267 | }
268 |
269 | prompt.input(
270 | ...,
271 | completions=completions,
272 | ...,
273 | )
274 |
275 | In this example, the user can select `1` or `2` as the first completion. If the user selects `1`, they can select `1.1`
276 | or `1.2`, then `1.2.1` and so on.
277 |
278 | The key of each entry is the completion that will be shown. The key is either :const:`None` if there are no further
279 | completions or a new :class:`dict`, where the key is the completion and the value is the next "layer", and so on.
280 |
281 | .. _completions_as_a_completer:
282 |
283 | Completions as a Completer
284 | **************************
285 |
286 | In the background your completions will be mapped to a :class:`~prompt_toolkit.completion.Completer`, provided by
287 | `prompt_toolkit`.
288 |
289 | If you need more customization, you can use a :class:`~prompt_toolkit.completion.Completer` given by `prompt-toolkit` or
290 | create your own completer. For more information on this process, read here:
291 | `Completions in prompt-toolkit `_.
292 |
293 | There are a number of completers available, for example:
294 |
295 | - :class:`~prompt_toolkit.completion.PathCompleter`
296 | - automatically complete file system paths
297 | - :class:`~prompt_toolkit.completion.ExecutableCompleter`
298 | - automatically complete executables in a file system
299 | - :class:`~prompt_toolkit.completion.WordCompleter`
300 | - As simple as it can get. Just completes the letters of the word, that are actually present (the `FuzzyCompleter`
301 | which `completions` uses in background completes based on a probability, and may show matches which are not
302 | exact).
303 | - ...
304 |
305 | To add your own completer to an :meth:`~ItsPrompt.prompt.Prompt.input` field, you can use the `completer` parameter:
306 |
307 | .. code-block:: python
308 |
309 | prompt.input(
310 | ...,
311 | completer=my_completer,
312 | ...,
313 | )
314 |
315 | .. note:: `completions` and `completer` are **mutually exclusive**!. You may only use one of them at a time.
316 |
--------------------------------------------------------------------------------
/docs/source/guide/prompt_types.rst:
--------------------------------------------------------------------------------
1 | Prompt Types in ItsPrompt
2 | =========================
3 |
4 | The following prompt types are available in ItsPrompt:
5 |
6 | .. contents:: Available Prompt Types
7 | :local:
8 | :depth: 2
9 |
10 | select
11 | ------
12 |
13 | .. image:: ../media/select.png
14 |
15 | .. code-block:: python
16 |
17 | ans = Prompt.select(
18 | question='What food would you like?',
19 | options=(Separator('The veggies'), 'Salad', Separator('The meaties'), 'Pizza', 'Burger'),
20 | default='Pizza',
21 | )
22 |
23 | To read more about the parameters of the `select` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.select`.
24 |
25 | raw_select
26 | ----------
27 |
28 | .. image:: ../media/raw_select.png
29 |
30 | .. code-block:: python
31 |
32 | ans = Prompt.raw_select(
33 | question='What pizza would you like?',
34 | options=('Salami', 'Hawaii', 'four-cheese'),
35 | allow_keyboard=True,
36 | )
37 |
38 | To read more about the parameters of the `raw_select` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.raw_select`.
39 |
40 | expand
41 | ------
42 |
43 | .. image:: ../media/expand.png
44 |
45 | .. code-block:: python
46 |
47 | ans = Prompt.expand(
48 | question='Where do you want your food to be delivered?',
49 | options=('my home', 'another home'),
50 | allow_keyboard=True,
51 | )
52 |
53 | To read more about the parameters of the `expand` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.expand`.
54 |
55 | checkbox
56 | --------
57 |
58 | .. image:: ../media/checkbox.png
59 |
60 | .. code-block:: python
61 |
62 | ans = Prompt.checkbox(
63 | question='What beverages would you like?',
64 | options=('Coke', 'Water', 'Juice'),
65 | default_checked=('Water',),
66 | disabled=("Coke",),
67 | min_selections=1,
68 | )
69 |
70 | To read more about the parameters of the `checkbox` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.checkbox`.
71 |
72 | confirm
73 | -------
74 |
75 | .. image:: ../media/confirm.png
76 |
77 | .. code-block:: python
78 |
79 | ans = Prompt.confirm(
80 | question='Is the information correct?',
81 | default=True,
82 | )
83 |
84 | To read more about the parameters of the `confirm` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.confirm`.
85 |
86 | input
87 | -----
88 |
89 | .. image:: ../media/input.png
90 |
91 | .. code-block:: python
92 |
93 | ans = Prompt.input(
94 | question='Please type your name',
95 | validate=input_not_empty,
96 | )
97 |
98 | To read more about the parameters of the `input` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.input`.
99 |
100 | .. _prompt_types_table:
101 |
102 | table
103 | -----
104 |
105 | .. image:: ../media/table.png
106 |
107 | .. code-block:: python
108 |
109 | data = DataFrame({
110 | 'Food': ['Pizza', 'Burger', 'Salad'],
111 | 'Qty': [1, 0, 0],
112 | })
113 |
114 | ans = Prompt.table(
115 | question='Please fill in your quantity',
116 | data=data,
117 | )
118 |
119 | To read more about the parameters of the `table` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.table`.
120 |
--------------------------------------------------------------------------------
/docs/source/guide/styling.rst:
--------------------------------------------------------------------------------
1 | Prompt Styling
2 | ==============
3 |
4 | `ItsPrompt` uses `prompt_toolkit` for its prompts. This module not only provides an easy way to interact with the
5 | command line, but also a full set of styling features.
6 |
7 | You can learn more about the available styling features in the
8 | `Styling Documentation `_
9 | of `prompt_toolkit`.
10 |
11 | `ItsPrompt` makes it a bit easier for you to style each component of a prompt. For every component, we give a separate
12 | attribute in the `PromptStyle` class, which you can style with valid `prompt_toolkit` styling:
13 |
14 | .. code-block:: python
15 |
16 | # examples for the different styling class components
17 | Prompt.raw_select(
18 | question='question',
19 | options=(
20 | 'option',
21 | 'selected_option',
22 | )
23 | )
24 |
25 | Prompt.input(
26 | question='question',
27 | default='grayout',
28 | validate=lambda x: 'error',
29 | )
30 |
31 | .. image:: ../media/styling_raw_select.png
32 |
33 | .. image:: ../media/styling_input.png
34 |
35 | +-------------------+---------------------------------------+-------------------------------------------------+
36 | | styling tag | default style | default design |
37 | +===================+=======================================+=================================================+
38 | | `question_mark` | `fg:ansigreen` | .. image:: ../media/styling/question_mark.png |
39 | +-------------------+---------------------------------------+-------------------------------------------------+
40 | | `question` | **\*** | .. image:: ../media/styling/question.png |
41 | +-------------------+---------------------------------------+-------------------------------------------------+
42 | | `option` | **\*** | .. image:: ../media/styling/option.png |
43 | +-------------------+---------------------------------------+-------------------------------------------------+
44 | | `selected_option` | `fg:ansicyan` | .. image:: ../media/styling/selected_option.png |
45 | +-------------------+---------------------------------------+-------------------------------------------------+
46 | | `tooltip` | `fg:ansibrightblue bg:ansiwhite bold` | .. image:: ../media/styling/tooltip.png |
47 | +-------------------+---------------------------------------+-------------------------------------------------+
48 | | `text` | **\*** | .. image:: ../media/styling/text.png |
49 | +-------------------+---------------------------------------+-------------------------------------------------+
50 | | `grayout` | `fg:ansibrightblack` | .. image:: ../media/styling/grayout.png |
51 | +-------------------+---------------------------------------+-------------------------------------------------+
52 | | `error` | `fg:ansiwhite bg:ansired bold` | .. image:: ../media/styling/error.png |
53 | +-------------------+---------------------------------------+-------------------------------------------------+
54 | | `separator` | `fg:ansibrightgreen` | .. image:: ../media/styling/separator.png |
55 | +-------------------+---------------------------------------+-------------------------------------------------+
56 |
57 | .. note:: **\***\ These values are not changed from the default `prompt_toolkit` values.
58 |
59 |
60 | To create your own style, there are two ways:
61 |
62 | Changing the Default Style
63 | --------------------------
64 |
65 | To change the default style, you need to import the :const:`~ItsPrompt.data.style.default_style` and change its values:
66 |
67 | .. code-block:: python
68 |
69 | from ItsPrompt.data.style import default_style
70 |
71 | default_style.error = 'fg:ansired bg:ansiwhite'
72 |
73 | This will automatically change the style of all prompts, which do not have an own style defined.
74 |
75 | Creating your own Style
76 | -----------------------
77 |
78 | To define your own style for a specific prompt, import :class:`~ItsPrompt.data.style.PromptStyle` and create an object.
79 | Then assign it to the `style` argument of any prompt.
80 |
81 | .. code-block:: python
82 |
83 | from ItsPrompt.data.style import PromptStyle
84 |
85 | my_style = PromptStyle(
86 | question_mark='fg:ansiblue',
87 | error='fg:ansired bg:ansiwhite',
88 | )
89 |
90 | .. note::
91 |
92 | If you omit a style attribute of your :class:`~ItsPrompt.data.style.PromptStyle`, this attribute **will not** use
93 | the one given by the :const:`~ItsPrompt.data.style.default_style`. Instead, it will use the default style given by
94 | `prompt_toolkit`.
95 |
96 | If you want to create your own style from the :const:`~ItsPrompt.data.style.default_style`, you can use the
97 | :meth:`~ItsPrompt.data.style.create_from_default` method:
98 |
99 | .. code-block:: python
100 |
101 | from ItsPrompt.data.style import create_from_default
102 |
103 | my_style = create_from_default()
104 |
105 | my_style.error = 'fg:ansired bg:ansiwhite'
106 |
107 | This will create a copy of the :const:`~ItsPrompt.data.style.default_style` and change its `error` attribute. All other
108 | attributes will remain the same as the :const:`~ItsPrompt.data.style.default_style`.
109 |
110 | .. note::
111 |
112 | Warning! Not copying the default style and changing it instead will result in all prompts using your changes, as a
113 | variable is by default not a copy, but a reference to the same object!
114 |
--------------------------------------------------------------------------------
/docs/source/guide/usage.rst:
--------------------------------------------------------------------------------
1 | ItsPrompt Usage
2 | ===============
3 |
4 | Question and Options
5 | --------------------
6 |
7 | Question
8 | ~~~~~~~~
9 |
10 | All basic prompt types have a ``question`` parameter which is the question that will be asked to the user.
11 |
12 | .. code-block:: python
13 |
14 | from itsprompt.prompt import Prompt
15 |
16 | ans = Prompt.input(question="What is your name?")
17 |
18 | print(ans)
19 |
20 | The above code will ask the user "What is your name?" and will store the user's input in the variable ``ans``.
21 |
22 | Options
23 | ~~~~~~~
24 |
25 | Prompt types which require the user to select an option from a list of options have an ``options`` parameter which is a
26 | list of options that the user can select from.
27 |
28 | .. code-block:: python
29 |
30 | from itsprompt.prompt import Prompt
31 |
32 | ans = Prompt.select(question="What is your favorite color?", options=["Red", "Green", "Blue"])
33 |
34 | print(ans)
35 |
36 | The above code will ask the user "What is your favorite color?" and will store the user's selected option in the
37 | variable ``ans``.
38 |
39 | There are many different ways to create options, including adding separators. To read more about them see
40 | :doc:`options_and_data`.
41 |
42 | Default Option
43 | ~~~~~~~~~~~~~~
44 |
45 | Prompt types which require options have the ability to have a default option. This is done by setting the ``default``
46 | parameter to the name or id of the default option.
47 |
48 | .. code-block:: python
49 |
50 | from itsprompt.prompt import Prompt
51 |
52 | ans = Prompt.select(question="What is your favorite color?", options=["Red", "Green", "Blue"], default="Green")
53 |
54 | print(ans)
55 |
56 | The above code will ask the user "What is your favorite color?" and will store the user's selected option in the
57 | variable ``ans``. The pointer will start on the option "Green".
58 |
59 | Disabled Options
60 | ~~~~~~~~~~~~~~~~
61 |
62 | Prompt types which require options have the ability to have disabled options. This is done by setting the ``disabled``
63 | parameter to a list of the names or ids of the disabled options.
64 |
65 | .. code-block:: python
66 |
67 | from itsprompt.prompt import Prompt
68 |
69 | ans = Prompt.select(question="What is your favorite color?", options=["Red", "Green", "Blue"], disabled=["Green"])
70 |
71 | print(ans)
72 |
73 | The above code will ask the user "What is your favorite color?" and will store the user's selected option in the
74 | variable ``ans``. The option "Green" will be grayed out and the user will not be able to select it.
75 |
76 | Text Input
77 | ~~~~~~~~~~
78 |
79 | Prompt types which require text input have a ``default`` parameter which is the default text that will be displayed in
80 | the input field.
81 |
82 | .. code-block:: python
83 |
84 | from itsprompt.prompt import Prompt
85 |
86 | ans = Prompt.input(question="What is your name?", default="John")
87 |
88 | print(ans)
89 |
90 | The above code will ask the user "What is your name?" and will store the user's input in the variable ``ans``. The input
91 | field will have the text "John" already in it.
92 |
93 | Styling
94 | -------
95 |
96 | Prompt types have a ``style`` parameter which defines the style of the prompt. To read more about styling see
97 | :doc:`styling`.
98 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. main page of ItsPrompt documentation
2 |
3 | Welcome to ItsPrompt's Documentation!
4 | =====================================
5 |
6 | .. image:: ./media/ItsPrompt.gif
7 |
8 | `ItsPrompt` is an easy-to-use module for managing prompts for the user. It allows you to ask the user for input in a fancy
9 | way, taking care of the problem of user input so you can focus on creating a great program.
10 |
11 | `ItsPrompt` offers many prompt types such as select, raw_select, expand, checkbox, confirm, input, and table. It also
12 | provides prompt autocompletion and validation, customizable style with `prompt_toolkit`, and a helpful toolbar with
13 | error messages.
14 |
15 | This documentation will guide you through the features and usage of `ItsPrompt`, helping you to integrate it into your
16 | projects seamlessly.
17 |
18 | Enjoy exploring `ItsPrompt`!
19 |
20 | Getting Started
21 | ===============
22 |
23 | To get started with `ItsPrompt`, follow the instructions in the :doc:`Getting Started Guide `.
24 |
25 | Table of Contents
26 | =================
27 |
28 | .. toctree::
29 | :maxdepth: 2
30 | :caption: Guides:
31 |
32 | guide/getting_started
33 | guide/usage
34 | guide/prompt_types
35 | guide/options_and_data
36 | guide/styling
37 |
38 | .. toctree::
39 | :maxdepth: 3
40 | :caption: API Documentation:
41 |
42 | api/prompt
43 | api/objects
44 |
45 | .. toctree::
46 | :maxdepth: 2
47 | :caption: Development Guide:
48 |
49 | development_guide/getting_started
50 | development_guide/documentation
51 |
--------------------------------------------------------------------------------
/docs/source/media/ItsPrompt.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/ItsPrompt.gif
--------------------------------------------------------------------------------
/docs/source/media/checkbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/checkbox.png
--------------------------------------------------------------------------------
/docs/source/media/confirm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/confirm.png
--------------------------------------------------------------------------------
/docs/source/media/expand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/expand.png
--------------------------------------------------------------------------------
/docs/source/media/input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/input.png
--------------------------------------------------------------------------------
/docs/source/media/raw_select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/raw_select.png
--------------------------------------------------------------------------------
/docs/source/media/select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/select.png
--------------------------------------------------------------------------------
/docs/source/media/styling/error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/error.png
--------------------------------------------------------------------------------
/docs/source/media/styling/grayout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/grayout.png
--------------------------------------------------------------------------------
/docs/source/media/styling/option.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/option.png
--------------------------------------------------------------------------------
/docs/source/media/styling/question.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/question.png
--------------------------------------------------------------------------------
/docs/source/media/styling/question_mark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/question_mark.png
--------------------------------------------------------------------------------
/docs/source/media/styling/selected_option.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/selected_option.png
--------------------------------------------------------------------------------
/docs/source/media/styling/separator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/separator.png
--------------------------------------------------------------------------------
/docs/source/media/styling/text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/text.png
--------------------------------------------------------------------------------
/docs/source/media/styling/tooltip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/tooltip.png
--------------------------------------------------------------------------------
/docs/source/media/styling_input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling_input.png
--------------------------------------------------------------------------------
/docs/source/media/styling_input_annotated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling_input_annotated.png
--------------------------------------------------------------------------------
/docs/source/media/styling_raw_select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling_raw_select.png
--------------------------------------------------------------------------------
/docs/source/media/styling_raw_select_annotated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling_raw_select_annotated.png
--------------------------------------------------------------------------------
/docs/source/media/table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/table.png
--------------------------------------------------------------------------------
/example.py:
--------------------------------------------------------------------------------
1 | # mypy: disable-error-code=assignment
2 | from ItsPrompt.objects.prompts.separator import Separator
3 |
4 | try:
5 | from pandas import DataFrame
6 |
7 | NO_PANDAS = False
8 | except ModuleNotFoundError:
9 | NO_PANDAS = True
10 |
11 | from ItsPrompt.prompt import Prompt
12 |
13 | # select
14 | ans = Prompt.select(
15 | 'What food would you like?',
16 | (Separator('The veggies'), 'Salad', Separator('The meaties'), 'Pizza', 'Burger'),
17 | default='Pizza',
18 | )
19 | print(ans)
20 |
21 | # raw_select
22 | ans = Prompt.raw_select(
23 | 'What pizza would you like?',
24 | ('Salami', 'Hawaii', 'four-cheese'),
25 | allow_keyboard=True,
26 | )
27 | print(ans)
28 |
29 | # checkbox
30 | ans = Prompt.checkbox(
31 | 'What beverages would you like?',
32 | ('Coke', 'Water', 'Juice'),
33 | default_checked=('Water',),
34 | disabled=("Coke",),
35 | min_selections=1,
36 | )
37 | print(ans)
38 |
39 | # expand
40 | ans = Prompt.expand(
41 | 'Where do you want your food to be delivered?',
42 | ('my home', 'another home'),
43 | allow_keyboard=True,
44 | )
45 | print(ans)
46 |
47 | # standard input with simple lambda validation
48 | ans = Prompt.input(
49 | 'Please type your id',
50 | validate=lambda x: "test" in x,
51 | )
52 |
53 | print(ans)
54 |
55 |
56 | # standard input with validation
57 | def input_not_empty(input: str) -> str | None:
58 | if len(input) == 0:
59 | return 'Input field can not be empty!'
60 |
61 | return None
62 |
63 |
64 | ans = Prompt.input(
65 | 'Please type your name',
66 | validate=input_not_empty,
67 | )
68 |
69 | print(ans)
70 |
71 | # standard input with validation and completion
72 | ans = Prompt.input(
73 | 'Please type your address',
74 | validate=input_not_empty,
75 | completions=['Mainstreet 4', 'Fifth way'],
76 | completion_show_multicolumn=True,
77 | )
78 | print(ans)
79 |
80 | # standard input with password (show_symbol)
81 | ans = Prompt.input(
82 | 'Please type your password',
83 | show_symbol='*',
84 | )
85 |
86 | print(ans)
87 |
88 | # confirm
89 | ans = Prompt.confirm(
90 | 'Is the information correct?',
91 | default=True,
92 | )
93 | print(ans)
94 |
95 | # table
96 | if not NO_PANDAS:
97 | data = DataFrame({
98 | 'Food': ['Pizza', 'Burger', 'Salad'],
99 | 'Qty': [1, 0, 0],
100 | })
101 |
102 | ans = Prompt.table(
103 | 'Please fill in your quantity',
104 | data,
105 | )
106 |
107 | print(ans)
108 |
109 | # styling
110 |
111 | # examples for the different styling class components
112 | Prompt.raw_select(question='question', options=(
113 | 'option',
114 | 'selected_option',
115 | ))
116 |
117 | Prompt.input(
118 | question='question',
119 | default='grayout',
120 | validate=lambda x: 'error',
121 | )
122 |
123 | # change default style
124 | from ItsPrompt.data.style import default_style
125 |
126 | default_style.error = 'fg:ansired bg:ansiwhite'
127 |
128 | # create your own style
129 | from ItsPrompt.data.style import PromptStyle
130 |
131 | my_style = PromptStyle(
132 | question_mark='fg:ansiblue',
133 | error='fg:ansired bg:ansiwhite',
134 | )
135 |
136 | # copy default style
137 | from ItsPrompt.data.style import create_from_default
138 |
139 | my_style = create_from_default()
140 |
141 | my_style.error = 'fg:ansired bg:ansiwhite'
142 |
--------------------------------------------------------------------------------
/examples/demo.py:
--------------------------------------------------------------------------------
1 | # mypy: disable-error-code=assignment
2 |
3 | # Demo file for ItsPrompt
4 | # This file demonstrates the usage of all available prompts.
5 | from ItsPrompt.objects.prompts.separator import Separator
6 |
7 | try:
8 | from pandas import DataFrame
9 |
10 | NO_PANDAS = False
11 | except ModuleNotFoundError:
12 | NO_PANDAS = True
13 |
14 | from ItsPrompt.prompt import Prompt
15 |
16 | # select
17 | ans = Prompt.select(
18 | 'What food would you like?',
19 | (Separator('The veggies'), 'Salad', Separator('The meaties'), 'Pizza', 'Burger'),
20 | default='Pizza',
21 | )
22 | print(ans)
23 |
24 | # raw_select
25 | ans = Prompt.raw_select(
26 | 'What pizza would you like?',
27 | ('Salami', 'Hawaii', 'four-cheese'),
28 | allow_keyboard=True,
29 | )
30 | print(ans)
31 |
32 | # checkbox
33 | ans = Prompt.checkbox(
34 | 'What beverages would you like?',
35 | ('Coke', 'Water', 'Juice'),
36 | default_checked=('Water',),
37 | disabled=("Coke",),
38 | min_selections=1,
39 | )
40 | print(ans)
41 |
42 | # expand
43 | ans = Prompt.expand(
44 | 'Where do you want your food to be delivered?',
45 | ('my home', 'another home'),
46 | allow_keyboard=True,
47 | )
48 | print(ans)
49 |
50 | # standard input with simple lambda validation
51 | ans = Prompt.input(
52 | 'Please type your id',
53 | validate=lambda x: "test" in x,
54 | )
55 |
56 | print(ans)
57 |
58 |
59 | # standard input with validation
60 | def input_not_empty(input: str) -> str | None:
61 | if len(input) == 0:
62 | return 'Input field can not be empty!'
63 |
64 | return None
65 |
66 |
67 | ans = Prompt.input(
68 | 'Please type your name',
69 | validate=input_not_empty,
70 | )
71 |
72 | print(ans)
73 |
74 | # standard input with validation and completion
75 | ans = Prompt.input(
76 | 'Please type your address',
77 | validate=input_not_empty,
78 | completions=['Mainstreet 4', 'Fifth way'],
79 | completion_show_multicolumn=True,
80 | )
81 | print(ans)
82 |
83 | # standard input with password (show_symbol)
84 | ans = Prompt.input(
85 | 'Please type your password',
86 | show_symbol='*',
87 | )
88 |
89 | print(ans)
90 |
91 | # confirm
92 | ans = Prompt.confirm(
93 | 'Is the information correct?',
94 | default=True,
95 | )
96 | print(ans)
97 |
98 | # table
99 | if not NO_PANDAS:
100 | data = DataFrame({
101 | 'Food': ['Pizza', 'Burger', 'Salad'],
102 | 'Qty': [1, 0, 0],
103 | })
104 |
105 | ans = Prompt.table(
106 | 'Please fill in your quantity',
107 | data,
108 | )
109 |
110 | print(ans)
111 |
--------------------------------------------------------------------------------
/examples/demos/checkbox_demo.py:
--------------------------------------------------------------------------------
1 | from ItsPrompt.prompt import Prompt
2 |
3 | # checkbox
4 | Prompt.checkbox(
5 | 'What beverages would you like?',
6 | ('Coke', 'Water', 'Juice'),
7 | default_checked=('Water',),
8 | disabled=("Coke",),
9 | min_selections=1,
10 | )
11 |
--------------------------------------------------------------------------------
/examples/demos/confirm_demo.py:
--------------------------------------------------------------------------------
1 | from ItsPrompt.prompt import Prompt
2 |
3 | # confirm
4 | Prompt.confirm(
5 | 'Is the information correct?',
6 | default=True,
7 | )
8 |
--------------------------------------------------------------------------------
/examples/demos/expand_demo.py:
--------------------------------------------------------------------------------
1 | from ItsPrompt.prompt import Prompt
2 |
3 | # expand
4 | Prompt.expand(
5 | 'Where do you want your food to be delivered?',
6 | ('my home', 'another home'),
7 | allow_keyboard=True,
8 | )
9 |
--------------------------------------------------------------------------------
/examples/demos/input_demo.py:
--------------------------------------------------------------------------------
1 | from ItsPrompt.prompt import Prompt
2 |
3 |
4 | # standard input validation
5 | def input_not_empty(input: str) -> str | None:
6 | if len(input) == 0:
7 | return 'Input field can not be empty!'
8 |
9 | return None
10 |
11 |
12 | # input
13 | Prompt.input(
14 | 'Please type your address',
15 | validate=input_not_empty,
16 | completions=['Mainstreet 4', 'Fifth way'],
17 | completion_show_multicolumn=True,
18 | )
19 |
--------------------------------------------------------------------------------
/examples/demos/raw_select_demo.py:
--------------------------------------------------------------------------------
1 | from ItsPrompt.prompt import Prompt
2 |
3 | # raw_select
4 | Prompt.raw_select(
5 | 'What pizza would you like?',
6 | ('Salami', 'Hawaii', 'four-cheese'),
7 | allow_keyboard=True,
8 | )
9 |
--------------------------------------------------------------------------------
/examples/demos/select_demo.py:
--------------------------------------------------------------------------------
1 | from ItsPrompt.objects.prompts.separator import Separator
2 | from ItsPrompt.prompt import Prompt
3 |
4 | Prompt.select(
5 | 'What food would you like?',
6 | (Separator('The veggies'), 'Salad', Separator('The meaties'), 'Pizza', 'Burger'),
7 | default='Pizza',
8 | )
9 |
--------------------------------------------------------------------------------
/examples/demos/table_demo.py:
--------------------------------------------------------------------------------
1 | from pandas import DataFrame
2 |
3 | from ItsPrompt.prompt import Prompt
4 |
5 | # table
6 | data = DataFrame({
7 | 'Food': ['Pizza', 'Burger', 'Salad'],
8 | 'Qty': [1, 0, 0],
9 | })
10 |
11 | Prompt.table(
12 | 'Please fill in your quantity',
13 | data,
14 | )
15 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "ItsPrompt"
7 | version = "1.5"
8 | authors = [
9 | { name = "ItsNameless" },
10 | ]
11 | description = "Prompting - the fancy way"
12 | readme = "README.md"
13 | requires-python = ">=3.8"
14 | dependencies = [
15 | "prompt-toolkit>=3.0.37,<4.0",
16 | ]
17 | classifiers = [
18 | "Programming Language :: Python :: 3",
19 | "License :: OSI Approved :: MIT License",
20 | "Operating System :: OS Independent",
21 | ]
22 | license = { file = "LICENSE" }
23 |
24 | [project.urls]
25 | Homepage = "https://github.com/TheItsProjects/ItsPrompt"
26 | Repository = "https://github.com/TheItsProjects/ItsPrompt"
27 | "Issue Tracker" = "https://github.com/TheItsProjects/ItsPrompt/issues"
28 |
29 | [tool.setuptools.packages.find]
30 | where = ["."]
31 | include = [
32 | "ItsPrompt",
33 | "ItsPrompt.*",
34 | ]
35 |
36 | [options.extras_require]
37 | df = "pandas>=1.5.3"
38 |
39 | [tool.pytest.ini_options]
40 | addopts = "--cov=ItsPrompt --cov-report term-missing"
41 | testpaths = [
42 | "tests",
43 | ]
44 | filterwarnings = [
45 | # ignore warning, see https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1696
46 | "ignore:There is no current event loop:DeprecationWarning",
47 | ]
48 |
49 | [tool.yapf]
50 | space_between_ending_comma_and_closing_bracket = false
51 | column_limit = 120
52 | each_dict_entry_on_separate_line = false
53 | dedent_closing_brackets = true
54 |
55 | [tool.mypy]
56 | packages = "ItsPrompt"
57 | explicit_package_bases = true
58 | check_untyped_defs = true
59 | exclude = ["docs", "venv"]
60 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | pre-commit==3.3.1
2 | pytest==7.3.1
3 | yapf==0.43.0
4 | mypy==1.2.0
5 | pytest-cov==4.0.0
6 | pandas-stubs==2.0.1.230501
7 | pandas>=1.5.3
8 | toml==0.10.2
9 | sphinx==7.2.6
10 | sphinx-rtd-theme==2.0.0
11 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | prompt-toolkit>=3.0.37,<4.0
2 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import NamedTuple
3 |
4 | import pytest
5 | from prompt_toolkit.application import create_app_session
6 | from prompt_toolkit.input import PipeInput, ansi_escape_sequences, create_pipe_input
7 | from prompt_toolkit.keys import Keys
8 | from prompt_toolkit.output import DummyOutput
9 |
10 |
11 | def key_converter(*keys: Keys | str) -> str:
12 | """
13 | Convert a key sequence to a sequence of stringified keys
14 |
15 | The keys may either be a `prompt_toolkit.keys.Keys` or a string, which will be interpreted as text to type.
16 |
17 | The returned string is usable with `PipeInput.send_text()`.
18 |
19 | :return: all given keys combined to a single string
20 | :rtype: str
21 | """
22 | out = ""
23 |
24 | for key in keys:
25 | if type(key) is Keys:
26 | out += ansi_escape_sequences.REVERSE_ANSI_SEQUENCES[key]
27 | else:
28 | out += key
29 |
30 | return out
31 |
32 |
33 | @pytest.fixture(autouse=True, scope="function")
34 | def mock_input():
35 | """
36 | Fixture for creating a dummy prompt session
37 |
38 | Terminal inputs can be created by using pipe_input.
39 | """
40 | with create_pipe_input() as pipe_input:
41 | with create_app_session(input=pipe_input, output=DummyOutput()):
42 | yield pipe_input
43 |
44 |
45 | @pytest.fixture(autouse=True, scope="function")
46 | def send_keys(mock_input: PipeInput):
47 | """
48 | Fixture for easily sending keys to the terminal session
49 |
50 | The returned callable can be called to convert a list of Keys or strings to one string, which will then be sent
51 | to the terminal session via PipeInput.
52 | """
53 | yield lambda *x: mock_input.send_text(key_converter(*x))
54 |
55 |
56 | class FakeTerminalSize(NamedTuple):
57 | columns: int
58 | lines: int
59 |
60 |
61 | @pytest.fixture(autouse=True)
62 | def mock_terminal_size(monkeypatch: pytest.MonkeyPatch):
63 |
64 | def get_fake_terminal_size(x=None):
65 | """
66 | Fixture for mocking the output of the `os.get_terminal_size()` method
67 |
68 | This fixture is needed in order for `TablePrompt()` to work.
69 | """
70 | return FakeTerminalSize(180, 60)
71 |
72 | monkeypatch.setattr(os, "get_terminal_size", get_fake_terminal_size)
73 |
--------------------------------------------------------------------------------
/tests/data/test_checkbox.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from ItsPrompt.data.checkbox import CheckboxOption, process_data
4 | from ItsPrompt.objects.prompts.separator import Separator
5 |
6 |
7 | def test_process_data_standard_options():
8 | options = ("first", "second", "third")
9 |
10 | result = [
11 | CheckboxOption("first", "first", False, False),
12 | CheckboxOption("second", "second", False, False),
13 | CheckboxOption("third", "third", False, False),
14 | ]
15 |
16 | ans = process_data(options)
17 |
18 | assert ans == result
19 |
20 |
21 | def test_process_data_tuple_options():
22 | options = (
23 | ("first", "1"),
24 | ("second", "2"),
25 | ("third", "3"),
26 | )
27 |
28 | result = [
29 | CheckboxOption("first", "1", False, False),
30 | CheckboxOption("second", "2", False, False),
31 | CheckboxOption("third", "3", False, False),
32 | ]
33 |
34 | ans = process_data(options) # type: ignore
35 |
36 | assert ans == result
37 |
38 |
39 | def test_process_data_raises_type_error():
40 | options = ("first", "second", 3)
41 |
42 | with pytest.raises(TypeError):
43 | ans = process_data(options) # type: ignore # mypy: ignore
44 |
45 |
46 | def test_process_data_with_separator():
47 | separator = Separator("second")
48 | options = ("first", separator, "third")
49 |
50 | result = [CheckboxOption("first", "first", False, False), separator, CheckboxOption("third", "third", False, False)]
51 |
52 | ans = process_data(options)
53 |
54 | assert ans.with_separators == result
55 |
--------------------------------------------------------------------------------
/tests/data/test_expand.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from ItsPrompt.data.expand import ExpandOption, process_data
4 | from ItsPrompt.objects.prompts.separator import Separator
5 |
6 |
7 | def test_process_data_standard_options():
8 | options = ("first", "second", "third")
9 |
10 | result = [
11 | ExpandOption("f", "first", "first", False),
12 | ExpandOption("s", "second", "second", False),
13 | ExpandOption("t", "third", "third", False),
14 | ExpandOption("h", "Help Menu, list or hide all options", "", False),
15 | ]
16 |
17 | ans = process_data(options)
18 |
19 | assert ans == result
20 |
21 |
22 | def test_process_data_tuple_options():
23 | options = (
24 | ("1", "first", "1"),
25 | ("2", "second", "2"),
26 | ("3", "third", "3"),
27 | )
28 |
29 | result = [
30 | ExpandOption("1", "first", "1", False),
31 | ExpandOption("2", "second", "2", False),
32 | ExpandOption("3", "third", "3", False),
33 | ExpandOption("h", "Help Menu, list or hide all options", "", False),
34 | ]
35 |
36 | ans = process_data(options)
37 |
38 | assert ans == result
39 |
40 |
41 | def test_process_data_raises_unique_keys():
42 | options = ("double", "double")
43 |
44 | with pytest.raises(ValueError):
45 | ans = process_data(options)
46 |
47 |
48 | def test_process_data_raises_key_too_long():
49 | options = (("tt", "too long", "tt"),)
50 |
51 | with pytest.raises(ValueError):
52 | ans = process_data(options)
53 |
54 |
55 | def test_process_data_raises_key_not_ascii():
56 | options = (("❓", "not ascii", "na"),)
57 |
58 | with pytest.raises(ValueError):
59 | ans = process_data(options)
60 |
61 |
62 | def test_process_data_raises_h_given():
63 | options = (("h", "h given", "h"),)
64 |
65 | with pytest.raises(ValueError):
66 | ans = process_data(options)
67 |
68 |
69 | def test_process_data_raises_type_error():
70 | options = (["i", "invalid"],)
71 |
72 | with pytest.raises(TypeError):
73 | ans = process_data(options) # type: ignore # mypy: ignore
74 |
75 |
76 | def test_process_data_with_separator():
77 | separator = Separator("second")
78 | options = ("first", separator, "third")
79 |
80 | result = [
81 | ExpandOption("f", "first", "first", False), separator,
82 | ExpandOption("t", "third", "third", False),
83 | ExpandOption(key='h', name='Help Menu, list or hide all options', id='', is_disabled=False)
84 | ]
85 |
86 | ans = process_data(options)
87 |
88 | assert ans.with_separators == result
89 |
--------------------------------------------------------------------------------
/tests/data/test_select.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from ItsPrompt.data.select import SelectOption, process_data
4 | from ItsPrompt.objects.prompts.separator import Separator
5 |
6 |
7 | def test_process_data_standard_options():
8 | options = ("first", "second", "third")
9 |
10 | result = [
11 | SelectOption("first", "first", False),
12 | SelectOption("second", "second", False),
13 | SelectOption("third", "third", False),
14 | ]
15 |
16 | ans = process_data(options)
17 |
18 | assert ans == result
19 |
20 |
21 | def test_process_data_tuple_options():
22 | options = (
23 | ("first", "1"),
24 | ("second", "2"),
25 | ("third", "3"),
26 | )
27 |
28 | result = [
29 | SelectOption("first", "1", False),
30 | SelectOption("second", "2", False),
31 | SelectOption("third", "3", False),
32 | ]
33 |
34 | ans = process_data(options) # type: ignore
35 |
36 | assert ans == result
37 |
38 |
39 | def test_process_data_raises_type_error():
40 | options = ("first", "second", 3)
41 |
42 | with pytest.raises(TypeError):
43 | ans = process_data(options) # type: ignore # mypy: ignore
44 |
45 |
46 | def test_process_data_with_separator():
47 | separator = Separator("second")
48 | options = ("first", separator, "third")
49 |
50 | result = [SelectOption("first", "first", False), separator, SelectOption("third", "third", False)]
51 |
52 | ans = process_data(options)
53 |
54 | assert ans.with_separators == result
55 |
--------------------------------------------------------------------------------
/tests/data/test_style.py:
--------------------------------------------------------------------------------
1 | from ItsPrompt.data.style import create_from_default, default_style
2 |
3 |
4 | def test_create_from_default():
5 | style = create_from_default()
6 |
7 | assert style == default_style
8 |
--------------------------------------------------------------------------------
/tests/data/test_table.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from pandas import DataFrame
4 |
5 | from ItsPrompt.data.table import Table
6 |
7 |
8 | def test_table_shortens_long_header(mock_terminal_size):
9 | width = os.get_terminal_size().columns
10 |
11 | data = DataFrame({"a" * (width - 1): ["first"]})
12 |
13 | table = Table(data)
14 |
15 | assert "." in table.get_table_as_str().split("\n")[1]
16 |
--------------------------------------------------------------------------------
/tests/prompts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/tests/prompts/__init__.py
--------------------------------------------------------------------------------
/tests/prompts/test_checkbox_prompt.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from prompt_toolkit.keys import Keys
3 |
4 | from ItsPrompt.objects.prompts.separator import Separator
5 | from ItsPrompt.prompt import Prompt
6 |
7 |
8 | # --- checkbox ---
9 | @pytest.mark.parametrize(
10 | "keys,i",
11 | [
12 | [(Keys.Enter,), ()],
13 | [(" ", Keys.Enter), (0,)],
14 | [(Keys.Down, " ", Keys.Enter), (1,)],
15 | [(" ", Keys.Up, " ", Keys.Enter), (0, 2)],
16 | ],
17 | )
18 | def test_checkbox(send_keys, keys: list[Keys | str], i: list[int]):
19 | options = ("first", "second", "third")
20 |
21 | send_keys(*keys)
22 |
23 | ans = Prompt.checkbox("", options)
24 |
25 | assert ans == [options[n] for n in i]
26 |
27 |
28 | @pytest.mark.parametrize(
29 | "keys,i",
30 | [
31 | [(Keys.Enter,), (0,)],
32 | [(" ", Keys.Enter), ()],
33 | [(Keys.Down, " ", Keys.Enter), (0, 1)],
34 | [(" ", Keys.Up, " ", Keys.Enter), (2,)],
35 | ],
36 | )
37 | def test_checkbox_with_default(send_keys, keys: list[Keys | str], i: list[int]):
38 | options = ("first", "second", "third")
39 |
40 | send_keys(*keys)
41 |
42 | ans = Prompt.checkbox("", options, default_checked=("first",))
43 |
44 | assert ans == [options[n] for n in i]
45 |
46 |
47 | def test_checkbox_raises_invalid_default():
48 | options = ("first", "second", "third")
49 |
50 | with pytest.raises(ValueError):
51 | ans = Prompt.checkbox("", options, default_checked=("invalid",))
52 |
53 |
54 | def test_checkbox_raises_keyboard_interrupt(send_keys):
55 | send_keys(Keys.ControlC)
56 |
57 | options = ("first", "second", "third")
58 |
59 | with pytest.raises(KeyboardInterrupt):
60 | ans = Prompt.checkbox("", options)
61 |
62 |
63 | # yapf: disable
64 | @pytest.mark.parametrize(
65 | "keys,i",
66 | [
67 | [(" ", Keys.Enter,), (1,)],
68 | [(Keys.Up, " ", Keys.Enter), (2,)],
69 | [(Keys.Down, Keys.Down, " ", Keys.Enter), (1,)]
70 | ]
71 | )
72 | # yapf: enable
73 | def test_checkbox_with_disabled(send_keys, keys: list[Keys | str], i: list[int]):
74 | options = ("first", "second", "third")
75 |
76 | send_keys(*keys)
77 |
78 | ans = Prompt.checkbox("", options, disabled=("first",))
79 |
80 | assert ans == [options[n] for n in i]
81 |
82 |
83 | def test_checkbox_with_separator(send_keys):
84 | options = ("first", "second", Separator("separator"), "third")
85 |
86 | send_keys(Keys.Down, Keys.Down, " ", Keys.Enter)
87 |
88 | ans = Prompt.checkbox("", options)
89 |
90 | assert ans == ["third"]
91 |
92 |
93 | def test_checkbox_raises_invalid_disabled():
94 | options = ("first", "second", "third")
95 | with pytest.raises(ValueError):
96 | ans = Prompt.checkbox("", options, disabled=("invalid",))
97 |
98 |
99 | # TODO check min selections with error box (visual)
100 |
--------------------------------------------------------------------------------
/tests/prompts/test_confirm_prompt.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from prompt_toolkit.keys import Keys
3 |
4 | from ItsPrompt.prompt import Prompt
5 |
6 |
7 | # --- confirm ---
8 | @pytest.mark.parametrize(
9 | "key,a",
10 | [
11 | ["y", True],
12 | ["n", False],
13 | ["ay", True],
14 | ],
15 | )
16 | def test_confirm(send_keys, key: str, a: bool):
17 | send_keys(key)
18 |
19 | ans = Prompt.confirm("")
20 |
21 | assert ans == a
22 |
23 |
24 | @pytest.mark.parametrize(
25 | "key,a",
26 | [
27 | ["y", True],
28 | ["n", False],
29 | [Keys.Enter, True],
30 | ],
31 | )
32 | def test_confirm_with_default(send_keys, key: str, a: bool):
33 | send_keys(key)
34 |
35 | ans = Prompt.confirm("", default=True)
36 |
37 | assert ans == a
38 |
39 |
40 | def test_confirm_raises_keyboard_interrupt(send_keys):
41 | send_keys(Keys.ControlC)
42 |
43 | with pytest.raises(KeyboardInterrupt):
44 | ans = Prompt.confirm("")
45 |
46 |
47 | # TODO confirm selections with error box (visual)
48 |
--------------------------------------------------------------------------------
/tests/prompts/test_expand_prompt.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from prompt_toolkit.keys import Keys
3 |
4 | from ItsPrompt.objects.prompts.separator import Separator
5 | from ItsPrompt.prompt import Prompt
6 |
7 |
8 | # --- Expand ---
9 | @pytest.mark.parametrize(
10 | "keys,i",
11 | [
12 | [("f", Keys.Enter), 0],
13 | [("sft", Keys.Enter), 2],
14 | [(Keys.Enter, "hs", Keys.Enter), 1],
15 | [(Keys.Up, Keys.Down, "99", "s", Keys.Enter), 1],
16 | ],
17 | )
18 | def test_expand(send_keys, keys: list[Keys | str], i: int):
19 | options = ("first", "second", "third")
20 |
21 | send_keys(*keys)
22 |
23 | ans = Prompt.expand("", options)
24 |
25 | assert ans == options[i]
26 |
27 |
28 | @pytest.mark.parametrize(
29 | "keys,i",
30 | [
31 | [("f", Keys.Enter), 0],
32 | [("sft", Keys.Enter), 2],
33 | [(Keys.Enter, "hs", Keys.Enter), 1],
34 | ],
35 | )
36 | def test_expand_with_default(send_keys, keys: list[Keys | str], i: int):
37 | options = ("first", "second", "third")
38 |
39 | send_keys(*keys)
40 |
41 | ans = Prompt.expand("", options, default="second")
42 |
43 | assert ans == options[i]
44 |
45 |
46 | @pytest.mark.parametrize(
47 | "keys,i",
48 | [
49 | [(Keys.Up, Keys.Enter), 2],
50 | [(Keys.Down, "hs", Keys.Enter), 1],
51 | ],
52 | )
53 | def test_expand_with_keyboard(send_keys, keys: list[Keys | str], i: int):
54 | options = ("first", "second", "third")
55 |
56 | send_keys(*keys)
57 |
58 | ans = Prompt.expand("", options, allow_keyboard=True)
59 |
60 | assert ans == options[i]
61 |
62 |
63 | def test_expand_raises_invalid_default():
64 | options = ("first", "second", "third")
65 |
66 | with pytest.raises(ValueError):
67 | ans = Prompt.expand("", options, default="invalid")
68 |
69 |
70 | def test_expand_raises_keyboard_interrupt(send_keys):
71 | send_keys(Keys.ControlC)
72 |
73 | options = ("first", "second", "third")
74 |
75 | with pytest.raises(KeyboardInterrupt):
76 | ans = Prompt.expand("", options)
77 |
78 |
79 | # yapf: disable
80 | @pytest.mark.parametrize(
81 | "keys,i",
82 | [
83 | [("f", Keys.Enter, "s", Keys.Enter), 1]
84 | ]
85 | )
86 | # yapf: enable
87 | def test_expand_with_disabled(send_keys, keys: list[Keys | str], i: int):
88 | options = ("first", "second", "third")
89 |
90 | send_keys(*keys)
91 |
92 | ans = Prompt.expand("", options, disabled=("first",))
93 |
94 | assert ans == options[i]
95 |
96 |
97 | def test_expand_raises_invalid_disabled():
98 | options = ("first", "second", "third")
99 | with pytest.raises(ValueError):
100 | ans = Prompt.expand("", options, disabled=("invalid",))
101 |
102 |
103 | def test_expand_raises_default_is_disabled():
104 | options = ("first", "second", "third")
105 | with pytest.raises(ValueError):
106 | ans = Prompt.expand("", options, default="first", disabled=("first",))
107 |
108 |
109 | @pytest.mark.parametrize(
110 | "keys,i",
111 | [
112 | [(Keys.Down, Keys.Enter), 1],
113 | [("s", Keys.Up, Keys.Up, Keys.Enter), 2], # s, h, t
114 | [(Keys.Down, Keys.Enter), 1]
115 | ]
116 | )
117 | def test_expand_with_disabled_and_keyboard(send_keys, keys: list[Keys | str], i: int):
118 | options = ("first", "second", "third")
119 |
120 | send_keys(*keys)
121 |
122 | ans = Prompt.expand("", options, disabled=("first",), allow_keyboard=True)
123 |
124 | assert ans == options[i]
125 |
126 |
127 | def test_expand_with_separator(send_keys):
128 | options = ("first", "second", Separator("separator"), "third")
129 |
130 | send_keys("h", "t", Keys.Enter)
131 |
132 | ans = Prompt.expand("", options)
133 |
134 | assert ans == "third"
135 |
--------------------------------------------------------------------------------
/tests/prompts/test_input_prompt.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from prompt_toolkit.completion import FuzzyWordCompleter
3 | from prompt_toolkit.keys import Keys
4 |
5 | from ItsPrompt.prompt import Prompt
6 |
7 |
8 | # --- input ---
9 | @pytest.mark.parametrize(
10 | "keys,a",
11 | [
12 | [(Keys.Enter,), ""],
13 | [("test", Keys.Enter), "test"],
14 | ],
15 | )
16 | def test_input(send_keys, keys: list[Keys | str], a: str):
17 | send_keys(*keys)
18 |
19 | ans = Prompt.input("")
20 |
21 | assert ans == a
22 |
23 |
24 | @pytest.mark.parametrize(
25 | "keys,a",
26 | [
27 | [(Keys.Enter,), "default"],
28 | [("test", Keys.Enter), "test"],
29 | ],
30 | )
31 | def test_input_with_default(send_keys, keys: list[Keys | str], a: str):
32 | send_keys(*keys)
33 |
34 | ans = Prompt.input("", default="default")
35 |
36 | assert ans == a
37 |
38 |
39 | KeysAltEnter = Keys.Escape, Keys.Enter # Vt100 terminals convert "alt+key" to "escape,key"
40 |
41 |
42 | @pytest.mark.parametrize(
43 | "keys,a",
44 | [
45 | [(*KeysAltEnter,), ""],
46 | [("test", Keys.Enter, *KeysAltEnter), "test\n"],
47 | ],
48 | )
49 | def test_input_with_multiline(send_keys, keys: list[Keys | str], a: str):
50 | send_keys(*keys)
51 |
52 | ans = Prompt.input("", multiline=True)
53 |
54 | assert ans == a
55 |
56 |
57 | def test_input_raises_two_completers_given():
58 | completions = ["first", "second", "third"]
59 | completer = FuzzyWordCompleter(completions)
60 |
61 | with pytest.raises(ValueError):
62 | ans = Prompt.input("", completions=completions, completer=completer)
63 |
64 |
65 | def test_input_raises_completer_and_show_symbol_given():
66 | completions = ["first", "second", "third"]
67 |
68 | with pytest.raises(ValueError):
69 | ans = Prompt.input("", completions=completions, show_symbol="*")
70 |
71 |
72 | def test_input_raises_keyboard_interrupt(send_keys):
73 | send_keys(Keys.ControlC)
74 |
75 | with pytest.raises(KeyboardInterrupt):
76 | ans = Prompt.input("")
77 |
78 |
79 | # TODO input show_symbol is showing symbol (visual)
80 | # TODO input completer/completions is working (visual, functional)
81 | # TODO input validation is showing error (visual)
82 |
--------------------------------------------------------------------------------
/tests/prompts/test_raw_select_prompt.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from prompt_toolkit.keys import Keys
3 |
4 | from ItsPrompt.objects.prompts.separator import Separator
5 | from ItsPrompt.prompt import Prompt
6 |
7 |
8 | # --- RawSelect ---
9 | @pytest.mark.parametrize(
10 | "keys,i",
11 | [
12 | [(Keys.Enter,), 0],
13 | [("1", Keys.Enter), 0],
14 | [("2", Keys.Enter), 1],
15 | [(Keys.Up, Keys.Down, "abc", "99", Keys.Enter), 0],
16 | ],
17 | )
18 | def test_raw_select(send_keys, keys: list[Keys | str], i: int):
19 | options = ("first", "second", "third")
20 |
21 | send_keys(*keys)
22 |
23 | ans = Prompt.raw_select("", options)
24 |
25 | assert ans == options[i]
26 |
27 |
28 | @pytest.mark.parametrize(
29 | "keys,i",
30 | [
31 | [(Keys.Enter,), 1],
32 | [("1", Keys.Enter), 0],
33 | [("2", Keys.Enter), 1],
34 | ],
35 | )
36 | def test_raw_select_with_default(send_keys, keys: list[Keys | str], i: int):
37 | options = ("first", "second", "third")
38 |
39 | send_keys(*keys)
40 |
41 | ans = Prompt.raw_select("", options, default="second")
42 |
43 | assert ans == options[i]
44 |
45 |
46 | @pytest.mark.parametrize(
47 | "keys,i",
48 | [
49 | [(Keys.Enter,), 0],
50 | [(Keys.Down, Keys.Enter), 1],
51 | [(Keys.Up, Keys.Enter), 2],
52 | ],
53 | )
54 | def test_raw_select_with_keyboard(send_keys, keys: list[Keys | str], i: int):
55 | options = ("first", "second", "third")
56 |
57 | send_keys(*keys)
58 |
59 | ans = Prompt.raw_select("", options, allow_keyboard=True)
60 |
61 | assert ans == options[i]
62 |
63 |
64 | def test_raw_select_raises_invalid_default():
65 | options = ("first", "second", "third")
66 |
67 | with pytest.raises(ValueError):
68 | ans = Prompt.raw_select("", options, default="invalid")
69 |
70 |
71 | def test_raw_select_raises_keyboard_interrupt(send_keys):
72 | send_keys(Keys.ControlC)
73 |
74 | options = ("first", "second", "third")
75 |
76 | with pytest.raises(KeyboardInterrupt):
77 | ans = Prompt.raw_select("", options)
78 |
79 |
80 | # yapf: disable
81 | @pytest.mark.parametrize(
82 | "keys,i",
83 | [
84 | [(Keys.Enter,), 1],
85 | [("1", Keys.Enter), 1]
86 | ]
87 | )
88 | # yapf: enable
89 | def test_raw_select_with_disabled(send_keys, keys: list[Keys | str], i: int):
90 | options = ("first", "second", "third")
91 |
92 | send_keys(*keys)
93 |
94 | ans = Prompt.raw_select("", options, disabled=("first",))
95 |
96 | assert ans == options[i]
97 |
98 |
99 | # yapf: disable
100 | @pytest.mark.parametrize(
101 | "keys,i",
102 | [
103 | [(Keys.Enter,), 1],
104 | [(Keys.Up, Keys.Enter), 2],
105 | [(Keys.Down, Keys.Down, Keys.Enter), 1]
106 | ]
107 | )
108 | # yapf: enable
109 | def test_raw_select_with_disabled_and_keyboard(send_keys, keys: list[Keys | str], i: int):
110 | options = ("first", "second", "third")
111 |
112 | send_keys(*keys)
113 |
114 | ans = Prompt.raw_select("", options, disabled=("first",), allow_keyboard=True)
115 |
116 | assert ans == options[i]
117 |
118 |
119 | def test_raw_select_with_separator(send_keys):
120 | options = ("first", "second", Separator("separator"), "third")
121 |
122 | send_keys("1", "2", Keys.Enter)
123 |
124 | ans = Prompt.raw_select("", options)
125 |
126 | assert ans == "second"
127 |
128 |
129 | def test_raw_select_raises_invalid_disabled():
130 | options = ("first", "second", "third")
131 | with pytest.raises(ValueError):
132 | ans = Prompt.raw_select("", options, disabled=("invalid",))
133 |
134 |
135 | def test_raw_select_raises_default_is_disabled():
136 | options = ("first", "second", "third")
137 | with pytest.raises(ValueError):
138 | ans = Prompt.raw_select("", options, default="first", disabled=("first",))
139 |
--------------------------------------------------------------------------------
/tests/prompts/test_select_prompt.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from prompt_toolkit.keys import Keys
3 |
4 | from ItsPrompt.objects.prompts.separator import Separator
5 | from ItsPrompt.prompt import Prompt
6 |
7 |
8 | # --- Select ---
9 | @pytest.mark.parametrize(
10 | "keys,i",
11 | [
12 | [(Keys.Enter,), 0],
13 | [(Keys.Down, Keys.Enter), 1],
14 | [(Keys.Down, Keys.Down, Keys.Enter), 2],
15 | [(Keys.Up, Keys.Enter), 2],
16 | ],
17 | )
18 | def test_select(send_keys, keys: list[Keys | str], i: int):
19 | options = ("first", "second", "third")
20 |
21 | send_keys(*keys)
22 |
23 | ans = Prompt.select("", options)
24 |
25 | assert ans == options[i]
26 |
27 |
28 | @pytest.mark.parametrize(
29 | "keys,i",
30 | [
31 | [(Keys.Enter,), 1],
32 | [(Keys.Down, Keys.Enter), 2],
33 | [(Keys.Down, Keys.Down, Keys.Enter), 0],
34 | [(Keys.Up, Keys.Enter), 0],
35 | ],
36 | )
37 | def test_select_with_default(send_keys, keys: list[Keys | str], i: int):
38 | options = ("first", "second", "third")
39 |
40 | send_keys(*keys)
41 |
42 | ans = Prompt.select("", options, default="second")
43 |
44 | assert ans == options[i]
45 |
46 |
47 | def test_select_raises_invalid_default():
48 | options = ("first", "second", "third")
49 | with pytest.raises(ValueError):
50 | ans = Prompt.select("", options, default="invalid")
51 |
52 |
53 | def test_select_raises_keyboard_interrupt(send_keys):
54 | send_keys(Keys.ControlC)
55 |
56 | options = ("first", "second", "third")
57 |
58 | with pytest.raises(KeyboardInterrupt):
59 | ans = Prompt.select("", options)
60 |
61 |
62 | # yapf: disable
63 | @pytest.mark.parametrize(
64 | "keys,i",
65 | [
66 | [(Keys.Enter,), 1],
67 | [(Keys.Up, Keys.Enter), 2],
68 | [(Keys.Down, Keys.Down, Keys.Enter), 1]
69 | ]
70 | )
71 | # yapf: enable
72 | def test_select_with_disabled(send_keys, keys: list[Keys | str], i: int):
73 | options = ("first", "second", "third")
74 |
75 | send_keys(*keys)
76 |
77 | ans = Prompt.select("", options, disabled=("first",))
78 |
79 | assert ans == options[i]
80 |
81 |
82 | def test_select_with_separator(send_keys):
83 | options = ("first", "second", Separator("separator"), "third")
84 |
85 | send_keys(Keys.Down, Keys.Down, Keys.Enter)
86 |
87 | ans = Prompt.select("", options)
88 |
89 | assert ans == "third"
90 |
91 |
92 | def test_select_raises_invalid_disabled():
93 | options = ("first", "second", "third")
94 | with pytest.raises(ValueError):
95 | ans = Prompt.select("", options, disabled=("invalid",))
96 |
97 |
98 | def test_select_raises_default_is_disabled():
99 | options = ("first", "second", "third")
100 | with pytest.raises(ValueError):
101 | ans = Prompt.select("", options, default="first", disabled=("first",))
102 |
--------------------------------------------------------------------------------
/tests/prompts/test_table_prompt.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from pandas import DataFrame
3 | from pandas.testing import assert_frame_equal
4 | from prompt_toolkit.keys import Keys
5 |
6 | from ItsPrompt.objects.prompts.type import TablePromptDict, TablePromptList
7 | from ItsPrompt.prompt import Prompt
8 |
9 |
10 | # --- table ---
11 | @pytest.mark.parametrize(
12 | "keys,a",
13 | [
14 | [
15 | (Keys.Enter,),
16 | DataFrame({"0": ["first", "second", "third"]}),
17 | ],
18 | [
19 | (Keys.Down, "new", Keys.Enter),
20 | DataFrame({"0": ["first", "secondnew", "third"]}),
21 | ],
22 | [
23 | (Keys.Left, Keys.Right, Keys.Up, Keys.Down, Keys.Enter),
24 | DataFrame({"0": ["first", "second", "third"]}),
25 | ],
26 | [
27 | (Keys.Backspace, Keys.Enter),
28 | DataFrame({"0": ["firs", "second", "third"]}),
29 | ],
30 | ],
31 | )
32 | def test_table_dataframe(send_keys, mock_terminal_size, keys: list[Keys | str], a: DataFrame):
33 | data = DataFrame(["first", "second", "third"])
34 |
35 | send_keys(*keys)
36 |
37 | ans = Prompt.table("", data)
38 |
39 | assert_frame_equal(ans, a)
40 |
41 |
42 | @pytest.mark.parametrize(
43 | "keys,a",
44 | [
45 | [
46 | (Keys.Enter,),
47 | {"0": ["first", "second", "third"]},
48 | ],
49 | [
50 | (Keys.Down, "new", Keys.Enter),
51 | {"0": ["first", "secondnew", "third"]},
52 | ],
53 | [
54 | (Keys.Left, Keys.Right, Keys.Up, Keys.Down, Keys.Enter),
55 | {"0": ["first", "second", "third"]},
56 | ],
57 | [
58 | (Keys.Backspace, Keys.Enter),
59 | {"0": ["firs", "second", "third"]},
60 | ],
61 | ],
62 | )
63 | def test_table_dictionary(send_keys, mock_terminal_size, keys: list[Keys | str], a: TablePromptDict):
64 | data = {"0": ["first", "second", "third"]}
65 |
66 | send_keys(*keys)
67 |
68 | ans = Prompt.table("", data)
69 |
70 | assert ans == a
71 |
72 |
73 | @pytest.mark.parametrize(
74 | "keys,a",
75 | [
76 | [
77 | (Keys.Enter,),
78 | [["first", "second", "third"]],
79 | ],
80 | [
81 | (Keys.Down, "new", Keys.Enter),
82 | [["first", "secondnew", "third"]],
83 | ],
84 | [
85 | (Keys.Left, Keys.Right, Keys.Up, Keys.Down, Keys.Enter),
86 | [["first", "second", "third"]],
87 | ],
88 | [
89 | (Keys.Backspace, Keys.Enter),
90 | [["firs", "second", "third"]],
91 | ],
92 | ],
93 | )
94 | def test_table_list(send_keys, mock_terminal_size, keys: list[Keys | str], a: TablePromptList):
95 | data = [["first", "second", "third"]]
96 |
97 | send_keys(*keys)
98 |
99 | ans = Prompt.table("", data)
100 |
101 | assert ans == a
102 |
103 |
104 | def test_table_raises_keyboard_interrupt(send_keys):
105 | send_keys(Keys.ControlC)
106 |
107 | data = DataFrame(["first", "second", "third"])
108 |
109 | with pytest.raises(KeyboardInterrupt):
110 | ans = Prompt.table("", data)
111 |
112 |
113 | def test_table_dictionary_raises_lists_not_same_lengths(send_keys):
114 | send_keys(Keys.ControlC)
115 |
116 | data = {"0": ["first", "second", "third"], "1": ["other first"]}
117 |
118 | with pytest.raises(ValueError):
119 | ans = Prompt.table("", data)
120 |
--------------------------------------------------------------------------------