diff --git a/README.rst b/README.rst index 5c5f2440..abf4f271 100644 --- a/README.rst +++ b/README.rst @@ -105,6 +105,38 @@ List questions can take one extra argument :code:`carousel=False`. If set to tru |inquirer list| +Keyed List +---- + +Shows a list of choices, and allows the selection of one of them using a corresponding key. + +Example: + +.. code:: python + + + import inquirer + questions = [ + inquirer.KeyedList('size', + message="What size do you need?", + choices=['Jumbo', 'Large', 'Standard', 'Medium', 'Small', 'Micro'], + ), + ] + answers = inquirer.prompt(questions) + +Keyed List choices use the first letter (lower case) or number of the provided choice by default. If multiple choices +share the same key, repeated presses of the same key will cycle through the matching entries. +Keyed Lists are otherwise similar to List but have one more additional argument, +:code:`auto_confirm=False`. If set to True, a key press with a matching entry will cause that entry to be instantly selected. + +Keys can explicitly be set by providing the label, value and key as a tuple: +:code:`choices=[('Jumbo', 'Jumbo', 'u'), ('Large', 'Large', 'a'), ...]` + + + +|inquirer list| + + Checkbox -------- diff --git a/poetry.lock b/poetry.lock index 05e8707c..9dcdb1a7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,6 +14,28 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + [[package]] name = "babel" version = "2.9.1" @@ -76,7 +98,7 @@ unicode_backport = ["unicodedata2"] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -164,7 +186,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "importlib-metadata" version = "4.2.0" description = "Read metadata from Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -176,6 +198,14 @@ zipp = ">=0.5" docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "jinja2" version = "3.0.3" @@ -233,7 +263,7 @@ python-versions = "*" name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -251,6 +281,21 @@ python-versions = "*" [package.dependencies] ptyprocess = ">=0.5" +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pre-commit-hooks" version = "4.1.0" @@ -271,6 +316,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "pycodestyle" version = "2.8.0" @@ -313,13 +366,35 @@ python-versions = ">=3.5" name = "pyparsing" version = "3.0.6" description = "Python parsing module" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pytest" +version = "7.1.2" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + [[package]] name = "python-editor" version = "1.0.4" @@ -546,6 +621,14 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "tornado" version = "6.1" @@ -558,7 +641,7 @@ python-versions = ">= 3.5" name = "typing-extensions" version = "4.0.1" description = "Backported and Experimental Type Hints for Python 3.6+" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -586,7 +669,7 @@ python-versions = "*" name = "zipp" version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -597,7 +680,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7" -content-hash = "cf8f7bf13cb0e983390dc7142e282399c81851b25f883b675d8cae6f9c0d495b" +content-hash = "6bde1042e4ce539716a6a94ec06319d03d7871483042361b7a6061ec7d218740" [metadata.files] alabaster = [ @@ -608,6 +691,14 @@ ansicon = [ {file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"}, {file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"}, ] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] babel = [ {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, @@ -664,6 +755,10 @@ importlib-metadata = [ {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, ] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] jinja2 = [ {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, @@ -681,6 +776,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, @@ -692,6 +790,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -703,6 +804,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, @@ -715,6 +819,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -727,6 +834,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -743,6 +853,10 @@ pexpect = [ {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, ] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] pre-commit-hooks = [ {file = "pre_commit_hooks-4.1.0-py2.py3-none-any.whl", hash = "sha256:ba95316b79038e56ce998cdacb1ce922831ac0e41744c77bcc2b9677bf183206"}, {file = "pre_commit_hooks-4.1.0.tar.gz", hash = "sha256:b6361865d1877c5da5ac3a944aab19ce6bd749a534d2ede28e683d07194a57e1"}, @@ -751,6 +865,10 @@ ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] pycodestyle = [ {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, @@ -771,6 +889,10 @@ pyparsing = [ {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] +pytest = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] python-editor = [ {file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"}, {file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"}, @@ -798,6 +920,10 @@ restructuredtext-lint = [ {file = "ruamel.yaml-0.17.19.tar.gz", hash = "sha256:b9ce9a925d0f0c35a1dbba56b40f253c53cd526b0fa81cf7b1d24996f28fb1d7"}, ] "ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"}, @@ -868,6 +994,10 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] tornado = [ {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, diff --git a/pyproject.toml b/pyproject.toml index 72a4e58e..0ff2e34e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ python = ">=3.7" blessed = ">=1.19.0" readchar = ">=2.0.1" python-editor = ">=1.0.4" +pytest = "^7.1.2" [tool.poetry.dev-dependencies] pexpect = ">=4.8.0" diff --git a/src/inquirer/questions.py b/src/inquirer/questions.py index 12d32c67..ca445b60 100644 --- a/src/inquirer/questions.py +++ b/src/inquirer/questions.py @@ -4,6 +4,7 @@ import errno import json import os +import re import sys import inquirer.errors as errors @@ -29,6 +30,25 @@ def __ne__(self, other): return not self.__eq__(other) +class KeyedValue(TaggedValue): + def __init__(self, label, value=None, key=None): + self.label = label + self.value = value or label + self.key = key or self._get_key(label) + + def _get_key(self, label): + _label = str(label) + k = re.search(r'([a-zA-Z\d])', _label) + if k: + return str(k.group(0)).lower() + return _label[0] + + def __str__(self): + return str(self.label) + + def __repr__(self): + return str(self.value) + class Question: kind = "base question" @@ -118,6 +138,21 @@ def __init__(self, name, message="", choices=None, default=None, ignore=False, v self.carousel = carousel +class KeyedList(Question): + kind = "keyed_list" + + def __init__(self, name, message="", choices=None, default=None, ignore=False, validate=True, carousel=False,auto_confirm=False): + super().__init__(name, message, choices, default, ignore, validate) + self.carousel = carousel + self.auto_confirm = auto_confirm # Auto Confirm selection on keypress + + + @property + def choices_generator(self): + for choice in self._solve(self._choices): + yield (KeyedValue(*choice) if isinstance(choice, (list, tuple, set)) and len(choice) >= 2 else KeyedValue(choice)) + + class Checkbox(Question): kind = "checkbox" @@ -227,7 +262,7 @@ def normalize_value(self, value): def question_factory(kind, *args, **kwargs): - for cl in (Text, Editor, Password, Confirm, List, Checkbox, Path): + for cl in (Text, Editor, Password, Confirm, List, KeyedList, Checkbox, Path): if cl.kind == kind: return cl(*args, **kwargs) raise errors.UnknownQuestionTypeError() diff --git a/src/inquirer/render/console/__init__.py b/src/inquirer/render/console/__init__.py index 74fdebad..4cf024f4 100644 --- a/src/inquirer/render/console/__init__.py +++ b/src/inquirer/render/console/__init__.py @@ -10,6 +10,7 @@ from inquirer.render.console._password import Password from inquirer.render.console._confirm import Confirm from inquirer.render.console._list import List +from inquirer.render.console._keyed_list import KeyedList from inquirer.render.console._checkbox import Checkbox from inquirer.render.console._path import Path @@ -146,6 +147,7 @@ def render_factory(self, question_type): "password": Password, "confirm": Confirm, "list": List, + "keyed_list": KeyedList, "checkbox": Checkbox, "path": Path, } diff --git a/src/inquirer/render/console/_keyed_list.py b/src/inquirer/render/console/_keyed_list.py new file mode 100644 index 00000000..fb929fc8 --- /dev/null +++ b/src/inquirer/render/console/_keyed_list.py @@ -0,0 +1,34 @@ +from readchar import key + +from inquirer import errors +from inquirer.render.console.base import MAX_OPTIONS_DISPLAYED_AT_ONCE +from inquirer.render.console.base import BaseConsoleRender +from inquirer.render.console.base import half_options +from inquirer.render.console._list import List + + +class KeyedList(List): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.current = self._current_index() + + def process_input(self, pressed): + super().process_input(pressed) + + keys = [choice.key for choice in self.question.choices] + + if pressed in keys: + self.current = self.get_next(keys, pressed) + + if self.question.auto_confirm: + value = self.question.choices[self.current] + + raise errors.EndOfInput(getattr(value, "value", value)) + + def get_next(self, keys, pressed): + try: + # Multiple entries with the same key? Get the 'next' one. + return keys.index(pressed, self.current + 1) + except ValueError: + # There isn't a next one, so get the first. + return keys.index(pressed) diff --git a/src/inquirer/shortcuts.py b/src/inquirer/shortcuts.py index 1bca921c..6ee40850 100644 --- a/src/inquirer/shortcuts.py +++ b/src/inquirer/shortcuts.py @@ -42,3 +42,9 @@ def path(message, render=None, **kwargs): render = render or ConsoleRender() question = questions.Path(name="", message=message, **kwargs) return render.render(question) + + +def keyed_list_input(message, render=None, **kwargs): + render = render or ConsoleRender() + question = questions.KeyedList(name="", message=message, **kwargs) + return render.render(question) diff --git a/tests/integration/console_render/test_keyed_list.py b/tests/integration/console_render/test_keyed_list.py new file mode 100644 index 00000000..465d4302 --- /dev/null +++ b/tests/integration/console_render/test_keyed_list.py @@ -0,0 +1,350 @@ +import unittest + +import pytest +from readchar import key + +import inquirer.questions as questions +import tests.integration.console_render.helper as helper +from inquirer.render import ConsoleRender + + +class KeyedListRenderTest(unittest.TestCase, helper.BaseTestCase): + def setUp(self): + self.base_setup() + + def tearDown(self): + self.base_teardown() + + def test_all_choices_are_shown(self): + stdin = helper.event_factory(key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "bazz"] + + question = questions.KeyedList(variable, message, choices=choices) + + sut = ConsoleRender(event_generator=stdin) + sut.render(question) + + self.assertInStdout(message) + for choice in choices: + self.assertInStdout(choice) + + def test_choose_the_first(self): + stdin = helper.event_factory(key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "bazz"] + + question = questions.KeyedList(variable, message, choices=choices) + + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "foo" + + def test_choose_the_second(self): + stdin = helper.event_factory(key.DOWN, key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "bazz"] + + question = questions.KeyedList(variable, message, choices=choices) + + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "bar" + + def test_choose_with_long_choices(self): + stdin = helper.event_factory( + key.DOWN, + key.DOWN, + key.DOWN, + key.DOWN, + key.DOWN, + key.DOWN, + key.DOWN, + key.DOWN, + key.DOWN, + key.DOWN, + key.ENTER, + ) + message = "Number message" + variable = "Number variable" + choices = list(range(15)) + + question = questions.KeyedList(variable, message, choices=choices) + + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == 10 + + def test_move_up(self): + stdin = helper.event_factory(key.DOWN, key.UP, key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "bazz"] + + question = questions.KeyedList(variable, message, choices=choices) + + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "foo" + + def test_move_down_carousel(self): + stdin = helper.event_factory(key.DOWN, key.DOWN, key.DOWN, key.DOWN, + key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "bazz"] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True) + + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "bar" + + def test_move_up_carousel(self): + stdin = helper.event_factory(key.UP, key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "bazz"] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True) + + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "bazz" + + def test_ctrl_c_breaks_execution(self): + stdin_array = [key.CTRL_C] + stdin = helper.event_factory(*stdin_array) + message = "Foo message" + variable = "Bar variable" + + question = questions.KeyedList(variable, message) + + sut = ConsoleRender(event_generator=stdin) + with pytest.raises(KeyboardInterrupt): + sut.render(question) + + def test_select_by_default_key_with_carousel_no_auto(self): + stdin = helper.event_factory(key.UP, key.UP, "f", key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "cat", "dog"] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "foo" + + def test_select_by_default_key_with_no_carousel_no_auto(self): + stdin = helper.event_factory(key.UP, key.DOWN, "d", key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "cat", "dog"] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "dog" + + def test_select_by_explicit_key_with_carousel_no_auto(self): + stdin = helper.event_factory(key.UP, key.UP, "f", key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = [ + ("foo", "foo", "f"), + ("bar", "bar", "b"), + ("cat", "cat", "c"), + ("dog", "dog", "d") + ] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "foo" + + def test_select_by_explicit_key_with_no_carousel_no_auto(self): + stdin = helper.event_factory(key.UP, key.DOWN, "d", key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = [ + ("foo", "foo", "f"), + ("bar", "bar", "b"), + ("cat", "cat", "c"), + ("dog", "dog", "d") + ] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "dog" + + def test_select_by_default_key_with_carousel_auto(self): + stdin = helper.event_factory(key.UP, key.UP, "f") + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "cat", "dog"] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True, + auto_confirm=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "foo" + + def test_select_by_default_key_with_no_carousel_auto(self): + stdin = helper.event_factory(key.UP, key.DOWN, "d") + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "cat", "dog"] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True, + auto_confirm=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "dog" + + def test_select_by_explicit_key_with_carousel_auto(self): + stdin = helper.event_factory(key.UP, key.UP, "f") + message = "Foo message" + variable = "Bar variable" + choices = [ + ("foo", "foo", "f"), + ("bar", "bar", "b"), + ("cat", "cat", "c"), + ("dog", "dog", "d") + ] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True, + auto_confirm=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "foo" + + def test_select_by_explicit_key_with_no_carousel_auto(self): + stdin = helper.event_factory(key.UP, key.DOWN, "d") + message = "Foo message" + variable = "Bar variable" + choices = [ + ("foo", "foo", "f"), + ("bar", "bar", "b"), + ("cat", "cat", "c"), + ("dog", "dog", "d") + ] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True, + auto_confirm=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "dog" + + def test_select_by_default_key_multiple_matches(self): + stdin = helper.event_factory(key.UP, key.UP, "b", "b", key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "baz", "cat", "dog", "car"] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "baz" + + def test_select_by_default_key_multiple_matches_variation(self): + stdin = helper.event_factory(key.UP, key.UP, "b", "c", "b", key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "baz", "cat", "dog", "car"] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "bar" + + def test_select_by_default_numeric_key_variation(self): + stdin = helper.event_factory(key.UP, key.UP, "1", "4", "3", key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["1. foo", "2. bar", "3. baz", "4. cat", "5. dog", "6. car"] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "3. baz" + + def test_select_by_default_numeric_key_multiple_matches_variation(self): + stdin = helper.event_factory(key.UP, key.UP, "1", "3", "1", key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["1. foo", "2. bar", "3. baz", "4. cat", "1. dog", "6. car"] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "1. dog" + + def test_select_by_default_numeric_key_variation_auto_select(self): + stdin = helper.event_factory(key.UP, key.UP, "1", "4", "3", key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["1. foo", "2. bar", "3. baz", "4. cat", "5. dog", "6. car"] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "3. baz" + + def test_select_by_default_numeric_key_explicit_value_auto_select_(self): + stdin = helper.event_factory(key.UP, key.UP, "4") + message = "Foo message" + variable = "Bar variable" + choices = [ + ("1. foo", "foo"), + ("2. bar", "bar"), + ("3. baz", "baz"), + ("4. cat", "cat"), + ("1. dog", "dog"), + ("6. car", "car") + ] + + question = questions.KeyedList(variable, message, choices=choices, + carousel=True, + auto_confirm=True) + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "cat" diff --git a/tests/unit/test_question.py b/tests/unit/test_question.py index 02cc2aa5..7423b2e4 100644 --- a/tests/unit/test_question.py +++ b/tests/unit/test_question.py @@ -362,6 +362,45 @@ def test_default_value_validation(self): questions.Path("path", default="~/.toggl_log") +class TestKeyedListQuestion(unittest.TestCase): + + def test_keyed_value_inits(self): + question = questions.KeyedValue("label") + assert question.label == "label" + assert question.value == "label" + assert question.key == "l" + + question = questions.KeyedValue("[l]abel") + assert question.label == "[l]abel" + assert question.value == "[l]abel" + assert question.key == "l" + + question = questions.KeyedValue("_Label") + assert question.label == "_Label" + assert question.value == "_Label" + assert question.key == "l" + + question = questions.KeyedValue("_label", 'o') + assert question.label == "_label" + assert question.value == "o" + assert question.key == "l" + + question = questions.KeyedValue("_label", "value", 'l') + assert question.label == "_label" + assert question.value == "value" + assert question.key == "l" + + question = questions.KeyedValue("_label", "value", 'o') + assert question.label == "_label" + assert question.value == "value" + assert question.key == "o" + + question = questions.KeyedValue("label", "value", 'o') + assert question.label == "label" + assert question.value == "value" + assert question.key == "o" + + def test_tagged_value(): tv = questions.TaggedValue("label", "value") @@ -370,3 +409,12 @@ def test_tagged_value(): assert tv.__eq__(tv) is True assert tv.__eq__("") is False assert tv.__ne__(tv) is False + + +def test_keyed_value_builtin_methods(): + ktv = questions.KeyedValue("label", "value", "l") + assert ktv.__str__() == "label" + assert ktv.__repr__() == "value" + assert ktv.__eq__(ktv) is True + assert ktv.__eq__("") is False + assert ktv.__ne__(ktv) is False diff --git a/tests/unit/test_shortcuts.py b/tests/unit/test_shortcuts.py index cebc344f..f7984fac 100644 --- a/tests/unit/test_shortcuts.py +++ b/tests/unit/test_shortcuts.py @@ -22,6 +22,7 @@ def render_mock(): (shortcuts.list_input, "list", "list_input message"), (shortcuts.checkbox, "checkbox", "checkbox message"), (shortcuts.path, "path", "path message"), + (shortcuts.keyed_list_input, "keyed_list", "keyed_list_input message") ], ) def test_shortcuts(func, kind, message, render_mock):