Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_internal/cli/parser.py: 22%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

210 statements  

1"""Base option parser setup""" 

2 

3from __future__ import annotations 

4 

5import logging 

6import optparse 

7import os 

8import re 

9import shutil 

10import sys 

11import textwrap 

12from collections.abc import Generator 

13from contextlib import suppress 

14from typing import TYPE_CHECKING, Any, NoReturn 

15 

16from pip._vendor.rich.markup import escape 

17from pip._vendor.rich.theme import Theme 

18 

19from pip._internal.cli.status_codes import UNKNOWN_ERROR 

20from pip._internal.configuration import Configuration, ConfigurationError 

21from pip._internal.utils.misc import redact_auth_from_url, strtobool 

22 

23if TYPE_CHECKING: 

24 from pip._internal.utils.logging import PipConsole 

25 

26logger = logging.getLogger(__name__) 

27 

28 

29class PrettyHelpFormatter(optparse.IndentedHelpFormatter): 

30 """A prettier/less verbose help formatter for optparse.""" 

31 

32 styles = { 

33 "optparse.shortargs": "green", 

34 "optparse.longargs": "cyan", 

35 "optparse.groups": "bold blue", 

36 "optparse.metavar": "yellow", 

37 } 

38 highlights = { 

39 r"\s(-{1}[\w]+[\w-]*)": "shortargs", # highlight -letter as short args 

40 r"\s(-{2}[\w]+[\w-]*)": "longargs", # highlight --words as long args 

41 } 

42 

43 def __init__(self, *args: Any, **kwargs: Any) -> None: 

44 # help position must be aligned with __init__.parseopts.description 

45 kwargs["max_help_position"] = 30 

46 kwargs["indent_increment"] = 1 

47 kwargs["width"] = shutil.get_terminal_size()[0] - 2 

48 super().__init__(*args, **kwargs) 

49 

50 def format_option_strings(self, option: optparse.Option) -> str: 

51 """Return a comma-separated list of option strings and metavars.""" 

52 opts = [] 

53 

54 if option._short_opts: 

55 opts.append(f"[optparse.shortargs]{option._short_opts[0]}[/]") 

56 if option._long_opts: 

57 opts.append(f"[optparse.longargs]{option._long_opts[0]}[/]") 

58 if len(opts) > 1: 

59 opts.insert(1, ", ") 

60 

61 if option.takes_value(): 

62 assert option.dest is not None 

63 metavar = option.metavar or option.dest.lower() 

64 opts.append(f" [optparse.metavar]<{escape(metavar.lower())}>[/]") 

65 

66 return "".join(opts) 

67 

68 def format_option(self, option: optparse.Option) -> str: 

69 """Overridden method with Rich support.""" 

70 # fmt: off 

71 result = [] 

72 opts = self.option_strings[option] 

73 opt_width = self.help_position - self.current_indent - 2 

74 # Remove the rich style tags before calculating width during 

75 # text wrap calculations. Also store the length removed to adjust 

76 # the padding in the else branch. 

77 stripped = re.sub(r"(\[[a-z.]+\])|(\[\/\])", "", opts) 

78 style_tag_length = len(opts) - len(stripped) 

79 if len(stripped) > opt_width: 

80 opts = "%*s%s\n" % (self.current_indent, "", opts) # noqa: UP031 

81 indent_first = self.help_position 

82 else: # start help on same line as opts 

83 opts = "%*s%-*s " % (self.current_indent, "", # noqa: UP031 

84 opt_width + style_tag_length, opts) 

85 indent_first = 0 

86 result.append(opts) 

87 if option.help: 

88 help_text = self.expand_default(option) 

89 help_lines = textwrap.wrap(help_text, self.help_width) 

90 result.append("%*s%s\n" % (indent_first, "", help_lines[0])) # noqa: UP031 

91 result.extend(["%*s%s\n" % (self.help_position, "", line) # noqa: UP031 

92 for line in help_lines[1:]]) 

93 elif opts[-1] != "\n": 

94 result.append("\n") 

95 return "".join(result) 

96 # fmt: on 

97 

98 def format_heading(self, heading: str) -> str: 

99 if heading == "Options": 

100 return "" 

101 return "[optparse.groups]" + escape(heading) + ":[/]\n" 

102 

103 def format_usage(self, usage: str) -> str: 

104 """ 

105 Ensure there is only one newline between usage and the first heading 

106 if there is no description. 

107 """ 

108 contents = self.indent_lines(textwrap.dedent(usage), " ") 

109 msg = f"\n[optparse.groups]Usage:[/] {escape(contents)}\n" 

110 return msg 

111 

112 def format_description(self, description: str | None) -> str: 

113 # leave full control over description to us 

114 if description: 

115 if hasattr(self.parser, "main"): 

116 label = "[optparse.groups]Commands:[/]" 

117 else: 

118 label = "[optparse.groups]Description:[/]" 

119 

120 # some doc strings have initial newlines, some don't 

121 description = description.lstrip("\n") 

122 # some doc strings have final newlines and spaces, some don't 

123 description = description.rstrip() 

124 # dedent, then reindent 

125 description = self.indent_lines(textwrap.dedent(description), " ") 

126 description = f"{label}\n{description}\n" 

127 return description 

128 else: 

129 return "" 

130 

131 def format_epilog(self, epilog: str | None) -> str: 

132 # leave full control over epilog to us 

133 if epilog: 

134 return escape(epilog) 

135 else: 

136 return "" 

137 

138 def expand_default(self, option: optparse.Option) -> str: 

139 """Overridden HelpFormatter.expand_default() which colorizes flags.""" 

140 help = escape(super().expand_default(option)) 

141 for regex, style in self.highlights.items(): 

142 help = re.sub(regex, rf"[optparse.{style}] \1[/]", help) 

143 return help 

144 

145 def indent_lines(self, text: str, indent: str) -> str: 

146 new_lines = [indent + line for line in text.split("\n")] 

147 return "\n".join(new_lines) 

148 

149 

150class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter): 

151 """Custom help formatter for use in ConfigOptionParser. 

152 

153 This is updates the defaults before expanding them, allowing 

154 them to show up correctly in the help listing. 

155 

156 Also redact auth from url type options 

157 """ 

158 

159 def expand_default(self, option: optparse.Option) -> str: 

160 default_values = None 

161 if self.parser is not None: 

162 assert isinstance(self.parser, ConfigOptionParser) 

163 self.parser._update_defaults(self.parser.defaults) 

164 assert option.dest is not None 

165 default_values = self.parser.defaults.get(option.dest) 

166 help_text = super().expand_default(option) 

167 

168 if default_values and option.metavar == "URL": 

169 if isinstance(default_values, str): 

170 default_values = [default_values] 

171 

172 # If its not a list, we should abort and just return the help text 

173 if not isinstance(default_values, list): 

174 default_values = [] 

175 

176 for val in default_values: 

177 help_text = help_text.replace(val, redact_auth_from_url(val)) 

178 

179 return help_text 

180 

181 

182class CustomOptionParser(optparse.OptionParser): 

183 def insert_option_group( 

184 self, idx: int, *args: Any, **kwargs: Any 

185 ) -> optparse.OptionGroup: 

186 """Insert an OptionGroup at a given position.""" 

187 group = self.add_option_group(*args, **kwargs) 

188 

189 self.option_groups.pop() 

190 self.option_groups.insert(idx, group) 

191 

192 return group 

193 

194 @property 

195 def option_list_all(self) -> list[optparse.Option]: 

196 """Get a list of all options, including those in option groups.""" 

197 res = self.option_list[:] 

198 for i in self.option_groups: 

199 res.extend(i.option_list) 

200 

201 return res 

202 

203 

204class ConfigOptionParser(CustomOptionParser): 

205 """Custom option parser which updates its defaults by checking the 

206 configuration files and environmental variables""" 

207 

208 def __init__( 

209 self, 

210 *args: Any, 

211 name: str, 

212 isolated: bool = False, 

213 **kwargs: Any, 

214 ) -> None: 

215 self.name = name 

216 self.config = Configuration(isolated) 

217 assert self.name 

218 super().__init__(*args, **kwargs) 

219 

220 def check_default(self, option: optparse.Option, key: str, val: Any) -> Any: 

221 try: 

222 return option.check_value(key, val) 

223 except optparse.OptionValueError as exc: 

224 logger.error( 

225 "An error occurred during configuration: %s. " 

226 "Check your pip configuration files or " 

227 "environment variables for this value. " 

228 "See https://pip.pypa.io/en/stable/topics/configuration/ " 

229 "for more information.", 

230 exc, 

231 ) 

232 sys.exit(3) 

233 

234 def _get_ordered_configuration_items( 

235 self, 

236 ) -> Generator[tuple[str, Any], None, None]: 

237 # Configuration gives keys in an unordered manner. Order them. 

238 override_order = ["global", self.name, ":env:"] 

239 

240 # Pool the options into different groups 

241 # Use a dict because we need to implement the fallthrough logic after PR 12201 

242 # was merged which removed the fallthrough logic for options 

243 section_items_dict: dict[str, dict[str, Any]] = { 

244 name: {} for name in override_order 

245 } 

246 

247 for _, value in self.config.items(): 

248 for section_key, val in value.items(): 

249 

250 section, key = section_key.split(".", 1) 

251 if section in override_order: 

252 section_items_dict[section][key] = val 

253 

254 # Now that we a dict of items per section, convert to list of tuples 

255 # Make sure we completely remove empty values again 

256 section_items = { 

257 name: [(k, v) for k, v in section_items_dict[name].items() if v] 

258 for name in override_order 

259 } 

260 

261 # Yield each group in their override order 

262 for section in override_order: 

263 yield from section_items[section] 

264 

265 def _update_defaults(self, defaults: dict[str, Any]) -> dict[str, Any]: 

266 """Updates the given defaults with values from the config files and 

267 the environ. Does a little special handling for certain types of 

268 options (lists).""" 

269 

270 # Accumulate complex default state. 

271 self.values = optparse.Values(self.defaults) 

272 late_eval = set() 

273 # Then set the options with those values 

274 for key, val in self._get_ordered_configuration_items(): 

275 # '--' because configuration supports only long names 

276 option = self.get_option("--" + key) 

277 

278 # Ignore options not present in this parser. E.g. non-globals put 

279 # in [global] by users that want them to apply to all applicable 

280 # commands. 

281 if option is None: 

282 continue 

283 

284 assert option.dest is not None 

285 

286 if option.action in ("store_true", "store_false"): 

287 try: 

288 val = strtobool(val) 

289 except ValueError: 

290 self.error( 

291 f"{val} is not a valid value for {key} option, " 

292 "please specify a boolean value like yes/no, " 

293 "true/false or 1/0 instead." 

294 ) 

295 elif option.action == "count": 

296 with suppress(ValueError): 

297 val = strtobool(val) 

298 with suppress(ValueError): 

299 val = int(val) 

300 if not isinstance(val, int) or val < 0: 

301 self.error( 

302 f"{val} is not a valid value for {key} option, " 

303 "please instead specify either a non-negative integer " 

304 "or a boolean value like yes/no or false/true " 

305 "which is equivalent to 1/0." 

306 ) 

307 elif option.action == "append": 

308 val = val.split() 

309 val = [self.check_default(option, key, v) for v in val] 

310 elif option.action == "callback": 

311 assert option.callback is not None 

312 late_eval.add(option.dest) 

313 opt_str = option.get_opt_string() 

314 val = option.convert_value(opt_str, val) 

315 # From take_action 

316 args = option.callback_args or () 

317 kwargs = option.callback_kwargs or {} 

318 option.callback(option, opt_str, val, self, *args, **kwargs) 

319 else: 

320 val = self.check_default(option, key, val) 

321 

322 defaults[option.dest] = val 

323 

324 for key in late_eval: 

325 defaults[key] = getattr(self.values, key) 

326 self.values = None 

327 return defaults 

328 

329 def get_default_values(self) -> optparse.Values: 

330 """Overriding to make updating the defaults after instantiation of 

331 the option parser possible, _update_defaults() does the dirty work.""" 

332 if not self.process_default_values: 

333 # Old, pre-Optik 1.5 behaviour. 

334 return optparse.Values(self.defaults) 

335 

336 # Load the configuration, or error out in case of an error 

337 try: 

338 self.config.load() 

339 except ConfigurationError as err: 

340 self.exit(UNKNOWN_ERROR, str(err)) 

341 

342 defaults = self._update_defaults(self.defaults.copy()) # ours 

343 for option in self._get_all_options(): 

344 assert option.dest is not None 

345 default = defaults.get(option.dest) 

346 if isinstance(default, str): 

347 opt_str = option.get_opt_string() 

348 defaults[option.dest] = option.check_value(opt_str, default) 

349 return optparse.Values(defaults) 

350 

351 def error(self, msg: str) -> NoReturn: 

352 self.print_usage(sys.stderr) 

353 self.exit(UNKNOWN_ERROR, f"{msg}\n") 

354 

355 def _create_console_for_help(self, file: Any = None) -> PipConsole: 

356 # This is unfortunate but necessary since arguments may have not been 

357 # parsed yet at this point, so detect --no-color manually. 

358 no_color = ( 

359 "--no-color" in sys.argv 

360 or bool(strtobool(os.environ.get("PIP_NO_COLOR", "no") or "no")) 

361 or "NO_COLOR" in os.environ 

362 ) 

363 # Imported lazily: rich's console stack is only needed when help is 

364 # actually rendered, not when this module is imported for `--version`. 

365 from pip._internal.utils.logging import PipConsole 

366 

367 return PipConsole( 

368 theme=Theme(PrettyHelpFormatter.styles), no_color=no_color, file=file 

369 ) 

370 

371 def print_usage(self, file: Any = None) -> None: 

372 if self.usage: 

373 console = self._create_console_for_help(file) 

374 console.print(self.get_usage(), highlight=False) 

375 

376 def print_help(self, file: Any = None) -> None: 

377 console = self._create_console_for_help(file) 

378 console.print(self.format_help().rstrip(), highlight=False)