utils.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. """Utils for the 'pylint-config' command."""
  5. from __future__ import annotations
  6. import sys
  7. from collections.abc import Callable
  8. from pathlib import Path
  9. from typing import TypeVar
  10. if sys.version_info >= (3, 8):
  11. from typing import Literal
  12. else:
  13. from typing_extensions import Literal
  14. if sys.version_info >= (3, 10):
  15. from typing import ParamSpec
  16. else:
  17. from typing_extensions import ParamSpec
  18. _P = ParamSpec("_P")
  19. _ReturnValueT = TypeVar("_ReturnValueT", bool, str)
  20. SUPPORTED_FORMATS = {"t", "toml", "i", "ini"}
  21. YES_NO_ANSWERS = {"y", "yes", "n", "no"}
  22. class InvalidUserInput(Exception):
  23. """Raised whenever a user input is invalid."""
  24. def __init__(self, valid_input: str, input_value: str, *args: object) -> None:
  25. self.valid = valid_input
  26. self.input = input_value
  27. super().__init__(*args)
  28. def should_retry_after_invalid_input(
  29. func: Callable[_P, _ReturnValueT]
  30. ) -> Callable[_P, _ReturnValueT]:
  31. """Decorator that handles InvalidUserInput exceptions and retries."""
  32. def inner_function(*args: _P.args, **kwargs: _P.kwargs) -> _ReturnValueT:
  33. called_once = False
  34. while True:
  35. try:
  36. return func(*args, **kwargs)
  37. except InvalidUserInput as exc:
  38. if called_once and exc.input == "exit()":
  39. print("Stopping 'pylint-config'.")
  40. sys.exit()
  41. print(f"Answer should be one of {exc.valid}.")
  42. print("Type 'exit()' if you want to exit the program.")
  43. called_once = True
  44. return inner_function
  45. @should_retry_after_invalid_input
  46. def get_and_validate_format() -> Literal["toml", "ini"]:
  47. """Make sure that the output format is either .toml or .ini."""
  48. # pylint: disable-next=bad-builtin
  49. format_type = input(
  50. "Please choose the format of configuration, (T)oml or (I)ni (.cfg): "
  51. ).lower()
  52. if format_type not in SUPPORTED_FORMATS:
  53. raise InvalidUserInput(", ".join(sorted(SUPPORTED_FORMATS)), format_type)
  54. if format_type.startswith("t"):
  55. return "toml"
  56. return "ini"
  57. @should_retry_after_invalid_input
  58. def validate_yes_no(question: str, default: Literal["yes", "no"] | None) -> bool:
  59. """Validate that a yes or no answer is correct."""
  60. question = f"{question} (y)es or (n)o "
  61. if default:
  62. question += f" (default={default}) "
  63. # pylint: disable-next=bad-builtin
  64. answer = input(question).lower()
  65. if not answer and default:
  66. answer = default
  67. if answer not in YES_NO_ANSWERS:
  68. raise InvalidUserInput(", ".join(sorted(YES_NO_ANSWERS)), answer)
  69. return answer.startswith("y")
  70. def get_minimal_setting() -> bool:
  71. """Ask the user if they want to use the minimal setting."""
  72. return validate_yes_no(
  73. "Do you want a minimal configuration without comments or default values?", "no"
  74. )
  75. def get_and_validate_output_file() -> tuple[bool, Path]:
  76. """Make sure that the output file is correct."""
  77. to_file = validate_yes_no("Do you want to write the output to a file?", "no")
  78. if not to_file:
  79. return False, Path()
  80. # pylint: disable-next=bad-builtin
  81. file_name = Path(input("What should the file be called: "))
  82. if file_name.exists():
  83. overwrite = validate_yes_no(
  84. f"{file_name} already exists. Are you sure you want to overwrite?", "no"
  85. )
  86. if not overwrite:
  87. return False, file_name
  88. return True, file_name
  89. return True, file_name