# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt """Utils for the 'pylint-config' command.""" from __future__ import annotations import sys from collections.abc import Callable from pathlib import Path from typing import TypeVar if sys.version_info >= (3, 8): from typing import Literal else: from typing_extensions import Literal if sys.version_info >= (3, 10): from typing import ParamSpec else: from typing_extensions import ParamSpec _P = ParamSpec("_P") _ReturnValueT = TypeVar("_ReturnValueT", bool, str) SUPPORTED_FORMATS = {"t", "toml", "i", "ini"} YES_NO_ANSWERS = {"y", "yes", "n", "no"} class InvalidUserInput(Exception): """Raised whenever a user input is invalid.""" def __init__(self, valid_input: str, input_value: str, *args: object) -> None: self.valid = valid_input self.input = input_value super().__init__(*args) def should_retry_after_invalid_input( func: Callable[_P, _ReturnValueT] ) -> Callable[_P, _ReturnValueT]: """Decorator that handles InvalidUserInput exceptions and retries.""" def inner_function(*args: _P.args, **kwargs: _P.kwargs) -> _ReturnValueT: called_once = False while True: try: return func(*args, **kwargs) except InvalidUserInput as exc: if called_once and exc.input == "exit()": print("Stopping 'pylint-config'.") sys.exit() print(f"Answer should be one of {exc.valid}.") print("Type 'exit()' if you want to exit the program.") called_once = True return inner_function @should_retry_after_invalid_input def get_and_validate_format() -> Literal["toml", "ini"]: """Make sure that the output format is either .toml or .ini.""" # pylint: disable-next=bad-builtin format_type = input( "Please choose the format of configuration, (T)oml or (I)ni (.cfg): " ).lower() if format_type not in SUPPORTED_FORMATS: raise InvalidUserInput(", ".join(sorted(SUPPORTED_FORMATS)), format_type) if format_type.startswith("t"): return "toml" return "ini" @should_retry_after_invalid_input def validate_yes_no(question: str, default: Literal["yes", "no"] | None) -> bool: """Validate that a yes or no answer is correct.""" question = f"{question} (y)es or (n)o " if default: question += f" (default={default}) " # pylint: disable-next=bad-builtin answer = input(question).lower() if not answer and default: answer = default if answer not in YES_NO_ANSWERS: raise InvalidUserInput(", ".join(sorted(YES_NO_ANSWERS)), answer) return answer.startswith("y") def get_minimal_setting() -> bool: """Ask the user if they want to use the minimal setting.""" return validate_yes_no( "Do you want a minimal configuration without comments or default values?", "no" ) def get_and_validate_output_file() -> tuple[bool, Path]: """Make sure that the output file is correct.""" to_file = validate_yes_no("Do you want to write the output to a file?", "no") if not to_file: return False, Path() # pylint: disable-next=bad-builtin file_name = Path(input("What should the file be called: ")) if file_name.exists(): overwrite = validate_yes_no( f"{file_name} already exists. Are you sure you want to overwrite?", "no" ) if not overwrite: return False, file_name return True, file_name return True, file_name