parser.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. """Base option parser setup"""
  2. from __future__ import annotations
  3. import logging
  4. import optparse
  5. import os
  6. import re
  7. import shutil
  8. import sys
  9. import textwrap
  10. from collections.abc import Generator
  11. from contextlib import suppress
  12. from typing import Any, NoReturn
  13. from pip._vendor.rich.markup import escape
  14. from pip._vendor.rich.theme import Theme
  15. from pip._internal.cli.status_codes import UNKNOWN_ERROR
  16. from pip._internal.configuration import Configuration, ConfigurationError
  17. from pip._internal.utils.logging import PipConsole
  18. from pip._internal.utils.misc import redact_auth_from_url, strtobool
  19. logger = logging.getLogger(__name__)
  20. class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
  21. """A prettier/less verbose help formatter for optparse."""
  22. styles = {
  23. "optparse.shortargs": "green",
  24. "optparse.longargs": "cyan",
  25. "optparse.groups": "bold blue",
  26. "optparse.metavar": "yellow",
  27. }
  28. highlights = {
  29. r"\s(-{1}[\w]+[\w-]*)": "shortargs", # highlight -letter as short args
  30. r"\s(-{2}[\w]+[\w-]*)": "longargs", # highlight --words as long args
  31. }
  32. def __init__(self, *args: Any, **kwargs: Any) -> None:
  33. # help position must be aligned with __init__.parseopts.description
  34. kwargs["max_help_position"] = 30
  35. kwargs["indent_increment"] = 1
  36. kwargs["width"] = shutil.get_terminal_size()[0] - 2
  37. super().__init__(*args, **kwargs)
  38. def format_option_strings(self, option: optparse.Option) -> str:
  39. """Return a comma-separated list of option strings and metavars."""
  40. opts = []
  41. if option._short_opts:
  42. opts.append(f"[optparse.shortargs]{option._short_opts[0]}[/]")
  43. if option._long_opts:
  44. opts.append(f"[optparse.longargs]{option._long_opts[0]}[/]")
  45. if len(opts) > 1:
  46. opts.insert(1, ", ")
  47. if option.takes_value():
  48. assert option.dest is not None
  49. metavar = option.metavar or option.dest.lower()
  50. opts.append(f" [optparse.metavar]<{escape(metavar.lower())}>[/]")
  51. return "".join(opts)
  52. def format_option(self, option: optparse.Option) -> str:
  53. """Overridden method with Rich support."""
  54. # fmt: off
  55. result = []
  56. opts = self.option_strings[option]
  57. opt_width = self.help_position - self.current_indent - 2
  58. # Remove the rich style tags before calculating width during
  59. # text wrap calculations. Also store the length removed to adjust
  60. # the padding in the else branch.
  61. stripped = re.sub(r"(\[[a-z.]+\])|(\[\/\])", "", opts)
  62. style_tag_length = len(opts) - len(stripped)
  63. if len(stripped) > opt_width:
  64. opts = "%*s%s\n" % (self.current_indent, "", opts) # noqa: UP031
  65. indent_first = self.help_position
  66. else: # start help on same line as opts
  67. opts = "%*s%-*s " % (self.current_indent, "", # noqa: UP031
  68. opt_width + style_tag_length, opts)
  69. indent_first = 0
  70. result.append(opts)
  71. if option.help:
  72. help_text = self.expand_default(option)
  73. help_lines = textwrap.wrap(help_text, self.help_width)
  74. result.append("%*s%s\n" % (indent_first, "", help_lines[0])) # noqa: UP031
  75. result.extend(["%*s%s\n" % (self.help_position, "", line) # noqa: UP031
  76. for line in help_lines[1:]])
  77. elif opts[-1] != "\n":
  78. result.append("\n")
  79. return "".join(result)
  80. # fmt: on
  81. def format_heading(self, heading: str) -> str:
  82. if heading == "Options":
  83. return ""
  84. return "[optparse.groups]" + escape(heading) + ":[/]\n"
  85. def format_usage(self, usage: str) -> str:
  86. """
  87. Ensure there is only one newline between usage and the first heading
  88. if there is no description.
  89. """
  90. contents = self.indent_lines(textwrap.dedent(usage), " ")
  91. msg = f"\n[optparse.groups]Usage:[/] {escape(contents)}\n"
  92. return msg
  93. def format_description(self, description: str | None) -> str:
  94. # leave full control over description to us
  95. if description:
  96. if hasattr(self.parser, "main"):
  97. label = "[optparse.groups]Commands:[/]"
  98. else:
  99. label = "[optparse.groups]Description:[/]"
  100. # some doc strings have initial newlines, some don't
  101. description = description.lstrip("\n")
  102. # some doc strings have final newlines and spaces, some don't
  103. description = description.rstrip()
  104. # dedent, then reindent
  105. description = self.indent_lines(textwrap.dedent(description), " ")
  106. description = f"{label}\n{description}\n"
  107. return description
  108. else:
  109. return ""
  110. def format_epilog(self, epilog: str | None) -> str:
  111. # leave full control over epilog to us
  112. if epilog:
  113. return escape(epilog)
  114. else:
  115. return ""
  116. def expand_default(self, option: optparse.Option) -> str:
  117. """Overridden HelpFormatter.expand_default() which colorizes flags."""
  118. help = escape(super().expand_default(option))
  119. for regex, style in self.highlights.items():
  120. help = re.sub(regex, rf"[optparse.{style}] \1[/]", help)
  121. return help
  122. def indent_lines(self, text: str, indent: str) -> str:
  123. new_lines = [indent + line for line in text.split("\n")]
  124. return "\n".join(new_lines)
  125. class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter):
  126. """Custom help formatter for use in ConfigOptionParser.
  127. This is updates the defaults before expanding them, allowing
  128. them to show up correctly in the help listing.
  129. Also redact auth from url type options
  130. """
  131. def expand_default(self, option: optparse.Option) -> str:
  132. default_values = None
  133. if self.parser is not None:
  134. assert isinstance(self.parser, ConfigOptionParser)
  135. self.parser._update_defaults(self.parser.defaults)
  136. assert option.dest is not None
  137. default_values = self.parser.defaults.get(option.dest)
  138. help_text = super().expand_default(option)
  139. if default_values and option.metavar == "URL":
  140. if isinstance(default_values, str):
  141. default_values = [default_values]
  142. # If its not a list, we should abort and just return the help text
  143. if not isinstance(default_values, list):
  144. default_values = []
  145. for val in default_values:
  146. help_text = help_text.replace(val, redact_auth_from_url(val))
  147. return help_text
  148. class CustomOptionParser(optparse.OptionParser):
  149. def insert_option_group(
  150. self, idx: int, *args: Any, **kwargs: Any
  151. ) -> optparse.OptionGroup:
  152. """Insert an OptionGroup at a given position."""
  153. group = self.add_option_group(*args, **kwargs)
  154. self.option_groups.pop()
  155. self.option_groups.insert(idx, group)
  156. return group
  157. @property
  158. def option_list_all(self) -> list[optparse.Option]:
  159. """Get a list of all options, including those in option groups."""
  160. res = self.option_list[:]
  161. for i in self.option_groups:
  162. res.extend(i.option_list)
  163. return res
  164. class ConfigOptionParser(CustomOptionParser):
  165. """Custom option parser which updates its defaults by checking the
  166. configuration files and environmental variables"""
  167. def __init__(
  168. self,
  169. *args: Any,
  170. name: str,
  171. isolated: bool = False,
  172. **kwargs: Any,
  173. ) -> None:
  174. self.name = name
  175. self.config = Configuration(isolated)
  176. assert self.name
  177. super().__init__(*args, **kwargs)
  178. def check_default(self, option: optparse.Option, key: str, val: Any) -> Any:
  179. try:
  180. return option.check_value(key, val)
  181. except optparse.OptionValueError as exc:
  182. print(f"An error occurred during configuration: {exc}")
  183. sys.exit(3)
  184. def _get_ordered_configuration_items(
  185. self,
  186. ) -> Generator[tuple[str, Any], None, None]:
  187. # Configuration gives keys in an unordered manner. Order them.
  188. override_order = ["global", self.name, ":env:"]
  189. # Pool the options into different groups
  190. # Use a dict because we need to implement the fallthrough logic after PR 12201
  191. # was merged which removed the fallthrough logic for options
  192. section_items_dict: dict[str, dict[str, Any]] = {
  193. name: {} for name in override_order
  194. }
  195. for _, value in self.config.items():
  196. for section_key, val in value.items():
  197. section, key = section_key.split(".", 1)
  198. if section in override_order:
  199. section_items_dict[section][key] = val
  200. # Now that we a dict of items per section, convert to list of tuples
  201. # Make sure we completely remove empty values again
  202. section_items = {
  203. name: [(k, v) for k, v in section_items_dict[name].items() if v]
  204. for name in override_order
  205. }
  206. # Yield each group in their override order
  207. for section in override_order:
  208. yield from section_items[section]
  209. def _update_defaults(self, defaults: dict[str, Any]) -> dict[str, Any]:
  210. """Updates the given defaults with values from the config files and
  211. the environ. Does a little special handling for certain types of
  212. options (lists)."""
  213. # Accumulate complex default state.
  214. self.values = optparse.Values(self.defaults)
  215. late_eval = set()
  216. # Then set the options with those values
  217. for key, val in self._get_ordered_configuration_items():
  218. # '--' because configuration supports only long names
  219. option = self.get_option("--" + key)
  220. # Ignore options not present in this parser. E.g. non-globals put
  221. # in [global] by users that want them to apply to all applicable
  222. # commands.
  223. if option is None:
  224. continue
  225. assert option.dest is not None
  226. if option.action in ("store_true", "store_false"):
  227. try:
  228. val = strtobool(val)
  229. except ValueError:
  230. self.error(
  231. f"{val} is not a valid value for {key} option, "
  232. "please specify a boolean value like yes/no, "
  233. "true/false or 1/0 instead."
  234. )
  235. elif option.action == "count":
  236. with suppress(ValueError):
  237. val = strtobool(val)
  238. with suppress(ValueError):
  239. val = int(val)
  240. if not isinstance(val, int) or val < 0:
  241. self.error(
  242. f"{val} is not a valid value for {key} option, "
  243. "please instead specify either a non-negative integer "
  244. "or a boolean value like yes/no or false/true "
  245. "which is equivalent to 1/0."
  246. )
  247. elif option.action == "append":
  248. val = val.split()
  249. val = [self.check_default(option, key, v) for v in val]
  250. elif option.action == "callback":
  251. assert option.callback is not None
  252. late_eval.add(option.dest)
  253. opt_str = option.get_opt_string()
  254. val = option.convert_value(opt_str, val)
  255. # From take_action
  256. args = option.callback_args or ()
  257. kwargs = option.callback_kwargs or {}
  258. option.callback(option, opt_str, val, self, *args, **kwargs)
  259. else:
  260. val = self.check_default(option, key, val)
  261. defaults[option.dest] = val
  262. for key in late_eval:
  263. defaults[key] = getattr(self.values, key)
  264. self.values = None
  265. return defaults
  266. def get_default_values(self) -> optparse.Values:
  267. """Overriding to make updating the defaults after instantiation of
  268. the option parser possible, _update_defaults() does the dirty work."""
  269. if not self.process_default_values:
  270. # Old, pre-Optik 1.5 behaviour.
  271. return optparse.Values(self.defaults)
  272. # Load the configuration, or error out in case of an error
  273. try:
  274. self.config.load()
  275. except ConfigurationError as err:
  276. self.exit(UNKNOWN_ERROR, str(err))
  277. defaults = self._update_defaults(self.defaults.copy()) # ours
  278. for option in self._get_all_options():
  279. assert option.dest is not None
  280. default = defaults.get(option.dest)
  281. if isinstance(default, str):
  282. opt_str = option.get_opt_string()
  283. defaults[option.dest] = option.check_value(opt_str, default)
  284. return optparse.Values(defaults)
  285. def error(self, msg: str) -> NoReturn:
  286. self.print_usage(sys.stderr)
  287. self.exit(UNKNOWN_ERROR, f"{msg}\n")
  288. def print_help(self, file: Any = None) -> None:
  289. # This is unfortunate but necessary since arguments may have not been
  290. # parsed yet at this point, so detect --no-color manually.
  291. no_color = (
  292. "--no-color" in sys.argv
  293. or bool(strtobool(os.environ.get("PIP_NO_COLOR", "no") or "no"))
  294. or "NO_COLOR" in os.environ
  295. )
  296. console = PipConsole(
  297. theme=Theme(PrettyHelpFormatter.styles), no_color=no_color, file=file
  298. )
  299. console.print(self.format_help().rstrip(), highlight=False)