utils.py 9.2 KB

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