diff --git a/pyproject.toml b/pyproject.toml index 38c76c5d54..65632a7ba2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,9 +41,10 @@ dependencies = [ "solc-select>=1.0.4,<2", "filelock>=3.15.1,<4", "ethereum-types>=0.2.1,<0.3", - "pyyaml>=6.0.2", - "types-pyyaml>=6.0.12.20240917", + "pyyaml>=6.0.2,<7", + "types-pyyaml>=6.0.12.20240917,<7", "pytest-json-report>=1.5.0,<2", + "questionary>=2.0.1,<3", ] [project.urls] diff --git a/src/cli/input/__init__.py b/src/cli/input/__init__.py new file mode 100644 index 0000000000..606dfbc931 --- /dev/null +++ b/src/cli/input/__init__.py @@ -0,0 +1,75 @@ +""" +A standard interface for interactive CLI inputs. +""" + +from .questionary_input_repository import QuestionaryInputRepository + +# Instantiate the input repository +input_repository = QuestionaryInputRepository() + + +def input_text(question: str) -> str: + """ + Ask a simple text input question. + + Args: + question (str): The question to ask. + + Returns: + str: The user's response. + """ + return input_repository.input_text(question) + + +def input_password(question: str) -> str: + """ + Ask a password input question (hidden text). + + Args: + question (str): The question to ask. + + Returns: + str: The user's response (password). + """ + return input_repository.input_password(question) + + +def input_select(question: str, choices: list) -> str: + """ + Ask a single-choice question from a list of options. + + Args: + question (str): The question to ask. + choices (list): A list of options for the user to choose from. + + Returns: + str: The selected choice. + """ + return input_repository.input_select(question, choices) + + +def input_checkbox(question: str, choices: list) -> list: + """ + Ask a multi-choice question and return a list of selected choices. + + Args: + question (str): The question to ask. + choices (list): A list of options for the user to choose from. + + Returns: + list: The list of selected choices. + """ + return input_repository.input_checkbox(question, choices) + + +def input_confirm(question: str) -> bool: + """ + Ask a yes/no confirmation question. + + Args: + question (str): The question to ask. + + Returns: + bool: True for 'yes', False for 'no'. + """ + return input_repository.input_confirm(question) diff --git a/src/cli/input/input_repository.py b/src/cli/input/input_repository.py new file mode 100644 index 0000000000..a07fa6a972 --- /dev/null +++ b/src/cli/input/input_repository.py @@ -0,0 +1,38 @@ +""" +An abstract base class for handling interactive CLI inputs. +""" + +from abc import ABC, abstractmethod +from typing import List + + +class InputRepository(ABC): + """ + Abstract base class for input handling. + This class defines the interface for different input types that can be swapped out. + """ + + @abstractmethod + def input_text(self, question: str) -> str: + """Ask a text input question.""" + pass + + @abstractmethod + def input_password(self, question: str) -> str: + """Ask a password input question (hidden).""" + pass + + @abstractmethod + def input_select(self, question: str, choices: List[str]) -> str: + """Ask a single-choice selection question.""" + pass + + @abstractmethod + def input_checkbox(self, question: str, choices: List[str]) -> List[str]: + """Ask a multi-choice question.""" + pass + + @abstractmethod + def input_confirm(self, question: str) -> bool: + """Ask a yes/no confirmation question.""" + pass diff --git a/src/cli/input/questionary_input_repository.py b/src/cli/input/questionary_input_repository.py new file mode 100644 index 0000000000..043cbaef73 --- /dev/null +++ b/src/cli/input/questionary_input_repository.py @@ -0,0 +1,42 @@ +""" +Interactive CLI inputs using questionary library. +See: https://questionary.readthedocs.io/ +""" + +from questionary import checkbox, confirm, password, select, text + +from .input_repository import InputRepository + + +class QuestionaryInputRepository(InputRepository): + """Repository for handling various types of user inputs using the Questionary library.""" + + def input_text(self, question: str) -> str: + """Ask a text input question. + See: https://questionary.readthedocs.io/en/stable/api.html#questionary.text + """ + return text(message=question).ask() + + def input_password(self, question: str) -> str: + """Ask a password input question (hidden). + See: https://questionary.readthedocs.io/en/stable/api.html#questionary.password + """ + return password(message=question).ask() + + def input_select(self, question: str, choices: list) -> str: + """Ask a single-choice selection question. + See: https://questionary.readthedocs.io/en/stable/api.html#questionary.select + """ + return select(message=question, choices=choices).ask() + + def input_checkbox(self, question: str, choices: list) -> list: + """Ask a multi-choice question. + See: https://questionary.readthedocs.io/en/stable/api.html#questionary.checkbox + """ + return checkbox(message=question, choices=choices).ask() + + def input_confirm(self, question: str) -> bool: + """Ask a yes/no confirmation question. + See: https://questionary.readthedocs.io/en/stable/api.html#questionary.confirm + """ + return confirm(message=question).ask() diff --git a/uv.lock b/uv.lock index 5ea4ef75fb..f1ef3aa65f 100644 --- a/uv.lock +++ b/uv.lock @@ -531,6 +531,7 @@ dependencies = [ { name = "pytest-metadata" }, { name = "pytest-xdist" }, { name = "pyyaml" }, + { name = "questionary" }, { name = "requests" }, { name = "requests-unixsocket2" }, { name = "rich" }, @@ -617,6 +618,7 @@ requires-dist = [ { name = "pytest-metadata", specifier = ">=3,<4" }, { name = "pytest-xdist", specifier = ">=3.3.1,<4" }, { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "questionary", specifier = ">=2.0.1" }, { name = "requests", specifier = ">=2.31.0,<3" }, { name = "requests-unixsocket2", specifier = ">=0.4.0" }, { name = "rich", specifier = ">=13.7.0,<14" }, @@ -1341,6 +1343,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/93/180be2342f89f16543ec4eb3f25083b5b84eba5378f68efff05409fb39a9/prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63", size = 423863 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/37/791f1a6edd13c61cac85282368aa68cb0f3f164440fdf60032f2cc6ca34e/prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305", size = 386414 }, +] + [[package]] name = "py-ecc" version = "7.0.1" @@ -1702,6 +1716,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, ] +[[package]] +name = "questionary" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/d0/d73525aeba800df7030ac187d09c59dc40df1c878b4fab8669bdc805535d/questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b", size = 24726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e7/2dd8f59d1d328773505f78b85405ddb1cfe74126425d076ce72e65540b8b/questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2", size = 34248 }, +] + [[package]] name = "regex" version = "2024.7.24" @@ -2050,6 +2076,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ea/da/0633223f30f5db6e52236567e09c28e37ff455b3dfbe0843029206e609e6/wcmatch-9.0-py3-none-any.whl", hash = "sha256:af25922e2b6dbd1550fa37a4c8de7dd558d6c1bb330c641de9b907b9776cb3c4", size = 39139 }, ] +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + [[package]] name = "webencodings" version = "0.5.1" diff --git a/whitelist.txt b/whitelist.txt index 4d1ac828e9..db53b0ef97 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -346,6 +346,7 @@ Pytest pytest's pytestArgs qGpsxSA +questionary quickstart radd randao