utils.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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 arguments/options parsing and handling."""
  5. from __future__ import annotations
  6. import re
  7. from collections.abc import Callable, Sequence
  8. from pathlib import Path
  9. from typing import TYPE_CHECKING, Any
  10. from pylint import extensions, utils
  11. from pylint.config.argument import (
  12. _CallableArgument,
  13. _ExtendArgument,
  14. _StoreArgument,
  15. _StoreNewNamesArgument,
  16. _StoreOldNamesArgument,
  17. _StoreTrueArgument,
  18. )
  19. from pylint.config.callback_actions import _CallbackAction
  20. from pylint.config.exceptions import ArgumentPreprocessingError
  21. if TYPE_CHECKING:
  22. from pylint.lint.run import Run
  23. def _convert_option_to_argument(
  24. opt: str, optdict: dict[str, Any]
  25. ) -> (
  26. _StoreArgument
  27. | _StoreTrueArgument
  28. | _CallableArgument
  29. | _StoreOldNamesArgument
  30. | _StoreNewNamesArgument
  31. | _ExtendArgument
  32. ):
  33. """Convert an optdict to an Argument class instance."""
  34. # Get the long and short flags
  35. flags = [f"--{opt}"]
  36. if "short" in optdict:
  37. flags += [f"-{optdict['short']}"]
  38. # Get the action type
  39. action = optdict.get("action", "store")
  40. if action == "store_true":
  41. return _StoreTrueArgument(
  42. flags=flags,
  43. action=action,
  44. default=optdict.get("default", True),
  45. arg_help=optdict.get("help", ""),
  46. hide_help=optdict.get("hide", False),
  47. section=optdict.get("group", None),
  48. )
  49. if not isinstance(action, str) and issubclass(action, _CallbackAction):
  50. return _CallableArgument(
  51. flags=flags,
  52. action=action,
  53. arg_help=optdict.get("help", ""),
  54. kwargs=optdict.get("kwargs", {}),
  55. hide_help=optdict.get("hide", False),
  56. section=optdict.get("group", None),
  57. metavar=optdict.get("metavar", None),
  58. )
  59. default = optdict["default"]
  60. if action == "extend":
  61. return _ExtendArgument(
  62. flags=flags,
  63. action=action,
  64. default=[] if default is None else default,
  65. arg_type=optdict["type"],
  66. choices=optdict.get("choices", None),
  67. arg_help=optdict.get("help", ""),
  68. metavar=optdict.get("metavar", ""),
  69. hide_help=optdict.get("hide", False),
  70. section=optdict.get("group", None),
  71. dest=optdict.get("dest", None),
  72. )
  73. if "kwargs" in optdict:
  74. if "old_names" in optdict["kwargs"]:
  75. return _StoreOldNamesArgument(
  76. flags=flags,
  77. default=default,
  78. arg_type=optdict["type"],
  79. choices=optdict.get("choices", None),
  80. arg_help=optdict.get("help", ""),
  81. metavar=optdict.get("metavar", ""),
  82. hide_help=optdict.get("hide", False),
  83. kwargs=optdict.get("kwargs", {}),
  84. section=optdict.get("group", None),
  85. )
  86. if "new_names" in optdict["kwargs"]:
  87. return _StoreNewNamesArgument(
  88. flags=flags,
  89. default=default,
  90. arg_type=optdict["type"],
  91. choices=optdict.get("choices", None),
  92. arg_help=optdict.get("help", ""),
  93. metavar=optdict.get("metavar", ""),
  94. hide_help=optdict.get("hide", False),
  95. kwargs=optdict.get("kwargs", {}),
  96. section=optdict.get("group", None),
  97. )
  98. if "dest" in optdict:
  99. return _StoreOldNamesArgument(
  100. flags=flags,
  101. default=default,
  102. arg_type=optdict["type"],
  103. choices=optdict.get("choices", None),
  104. arg_help=optdict.get("help", ""),
  105. metavar=optdict.get("metavar", ""),
  106. hide_help=optdict.get("hide", False),
  107. kwargs={"old_names": [optdict["dest"]]},
  108. section=optdict.get("group", None),
  109. )
  110. return _StoreArgument(
  111. flags=flags,
  112. action=action,
  113. default=default,
  114. arg_type=optdict["type"],
  115. choices=optdict.get("choices", None),
  116. arg_help=optdict.get("help", ""),
  117. metavar=optdict.get("metavar", ""),
  118. hide_help=optdict.get("hide", False),
  119. section=optdict.get("group", None),
  120. )
  121. def _parse_rich_type_value(value: Any) -> str:
  122. """Parse rich (toml) types into strings."""
  123. if isinstance(value, (list, tuple)):
  124. return ",".join(_parse_rich_type_value(i) for i in value)
  125. if isinstance(value, re.Pattern):
  126. return str(value.pattern)
  127. if isinstance(value, dict):
  128. return ",".join(f"{k}:{v}" for k, v in value.items())
  129. return str(value)
  130. # pylint: disable-next=unused-argument
  131. def _init_hook(run: Run, value: str | None) -> None:
  132. """Execute arbitrary code from the init_hook.
  133. This can be used to set the 'sys.path' for example.
  134. """
  135. assert value is not None
  136. exec(value) # pylint: disable=exec-used
  137. def _set_rcfile(run: Run, value: str | None) -> None:
  138. """Set the rcfile."""
  139. assert value is not None
  140. run._rcfile = value
  141. def _set_output(run: Run, value: str | None) -> None:
  142. """Set the output."""
  143. assert value is not None
  144. run._output = value
  145. def _add_plugins(run: Run, value: str | None) -> None:
  146. """Add plugins to the list of loadable plugins."""
  147. assert value is not None
  148. run._plugins.extend(utils._splitstrip(value))
  149. def _set_verbose_mode(run: Run, value: str | None) -> None:
  150. assert value is None
  151. run.verbose = True
  152. def _enable_all_extensions(run: Run, value: str | None) -> None:
  153. """Enable all extensions."""
  154. assert value is None
  155. for filename in Path(extensions.__file__).parent.iterdir():
  156. if filename.suffix == ".py" and not filename.stem.startswith("_"):
  157. extension_name = f"pylint.extensions.{filename.stem}"
  158. if extension_name not in run._plugins:
  159. run._plugins.append(extension_name)
  160. PREPROCESSABLE_OPTIONS: dict[
  161. str, tuple[bool, Callable[[Run, str | None], None], int]
  162. ] = { # pylint: disable=consider-using-namedtuple-or-dataclass
  163. # pylint: disable=useless-suppression, wrong-spelling-in-comment
  164. # Argparse by default allows abbreviations. It behaves differently
  165. # if you turn this off, so we also turn it on. We mimic this
  166. # by allowing some abbreviations or incorrect spelling here.
  167. # The integer at the end of the tuple indicates how many letters
  168. # should match, include the '-'. 0 indicates a full match.
  169. #
  170. # Clashes with --init-(import)
  171. "--init-hook": (True, _init_hook, 8),
  172. # Clashes with --r(ecursive)
  173. "--rcfile": (True, _set_rcfile, 4),
  174. # Clashes with --output(-format)
  175. "--output": (True, _set_output, 0),
  176. # Clashes with --lo(ng-help)
  177. "--load-plugins": (True, _add_plugins, 5),
  178. # Clashes with --v(ariable-rgx)
  179. "--verbose": (False, _set_verbose_mode, 4),
  180. "-v": (False, _set_verbose_mode, 2),
  181. # Clashes with --enable
  182. "--enable-all-extensions": (False, _enable_all_extensions, 9),
  183. }
  184. # pylint: enable=wrong-spelling-in-comment
  185. def _preprocess_options(run: Run, args: Sequence[str]) -> list[str]:
  186. """Pre-process options before full config parsing has started."""
  187. processed_args: list[str] = []
  188. i = 0
  189. while i < len(args):
  190. argument = args[i]
  191. if not argument.startswith("-"):
  192. processed_args.append(argument)
  193. i += 1
  194. continue
  195. try:
  196. option, value = argument.split("=", 1)
  197. except ValueError:
  198. option, value = argument, None
  199. matched_option = None
  200. for option_name, data in PREPROCESSABLE_OPTIONS.items():
  201. to_match = data[2]
  202. if to_match == 0:
  203. if option == option_name:
  204. matched_option = option_name
  205. elif option.startswith(option_name[:to_match]):
  206. matched_option = option_name
  207. if matched_option is None:
  208. processed_args.append(argument)
  209. i += 1
  210. continue
  211. takearg, cb, _ = PREPROCESSABLE_OPTIONS[matched_option]
  212. if takearg and value is None:
  213. i += 1
  214. if i >= len(args) or args[i].startswith("-"):
  215. raise ArgumentPreprocessingError(f"Option {option} expects a value")
  216. value = args[i]
  217. elif not takearg and value is not None:
  218. raise ArgumentPreprocessingError(f"Option {option} doesn't expect a value")
  219. cb(run, value)
  220. i += 1
  221. return processed_args