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

202 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 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.logging import PipConsole 

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

23 

24logger = logging.getLogger(__name__) 

25 

26 

27class PrettyHelpFormatter(optparse.IndentedHelpFormatter): 

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

29 

30 styles = { 

31 "optparse.shortargs": "green", 

32 "optparse.longargs": "cyan", 

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

34 "optparse.metavar": "yellow", 

35 } 

36 highlights = { 

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

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

39 } 

40 

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

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

43 kwargs["max_help_position"] = 30 

44 kwargs["indent_increment"] = 1 

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

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

47 

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

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

50 opts = [] 

51 

52 if option._short_opts: 

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

54 if option._long_opts: 

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

56 if len(opts) > 1: 

57 opts.insert(1, ", ") 

58 

59 if option.takes_value(): 

60 assert option.dest is not None 

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

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

63 

64 return "".join(opts) 

65 

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

67 """Overridden method with Rich support.""" 

68 # fmt: off 

69 result = [] 

70 opts = self.option_strings[option] 

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

72 # Remove the rich style tags before calculating width during 

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

74 # the padding in the else branch. 

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

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

77 if len(stripped) > opt_width: 

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

79 indent_first = self.help_position 

80 else: # start help on same line as opts 

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

82 opt_width + style_tag_length, opts) 

83 indent_first = 0 

84 result.append(opts) 

85 if option.help: 

86 help_text = self.expand_default(option) 

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

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

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

90 for line in help_lines[1:]]) 

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

92 result.append("\n") 

93 return "".join(result) 

94 # fmt: on 

95 

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

97 if heading == "Options": 

98 return "" 

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

100 

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

102 """ 

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

104 if there is no description. 

105 """ 

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

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

108 return msg 

109 

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

111 # leave full control over description to us 

112 if description: 

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

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

115 else: 

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

117 

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

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

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

121 description = description.rstrip() 

122 # dedent, then reindent 

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

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

125 return description 

126 else: 

127 return "" 

128 

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

130 # leave full control over epilog to us 

131 if epilog: 

132 return escape(epilog) 

133 else: 

134 return "" 

135 

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

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

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

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

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

141 return help 

142 

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

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

145 return "\n".join(new_lines) 

146 

147 

148class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter): 

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

150 

151 This is updates the defaults before expanding them, allowing 

152 them to show up correctly in the help listing. 

153 

154 Also redact auth from url type options 

155 """ 

156 

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

158 default_values = None 

159 if self.parser is not None: 

160 assert isinstance(self.parser, ConfigOptionParser) 

161 self.parser._update_defaults(self.parser.defaults) 

162 assert option.dest is not None 

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

164 help_text = super().expand_default(option) 

165 

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

167 if isinstance(default_values, str): 

168 default_values = [default_values] 

169 

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

171 if not isinstance(default_values, list): 

172 default_values = [] 

173 

174 for val in default_values: 

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

176 

177 return help_text 

178 

179 

180class CustomOptionParser(optparse.OptionParser): 

181 def insert_option_group( 

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

183 ) -> optparse.OptionGroup: 

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

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

186 

187 self.option_groups.pop() 

188 self.option_groups.insert(idx, group) 

189 

190 return group 

191 

192 @property 

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

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

195 res = self.option_list[:] 

196 for i in self.option_groups: 

197 res.extend(i.option_list) 

198 

199 return res 

200 

201 

202class ConfigOptionParser(CustomOptionParser): 

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

204 configuration files and environmental variables""" 

205 

206 def __init__( 

207 self, 

208 *args: Any, 

209 name: str, 

210 isolated: bool = False, 

211 **kwargs: Any, 

212 ) -> None: 

213 self.name = name 

214 self.config = Configuration(isolated) 

215 

216 assert self.name 

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

218 

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

220 try: 

221 return option.check_value(key, val) 

222 except optparse.OptionValueError as exc: 

223 print(f"An error occurred during configuration: {exc}") 

224 sys.exit(3) 

225 

226 def _get_ordered_configuration_items( 

227 self, 

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

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

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

231 

232 # Pool the options into different groups 

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

234 # was merged which removed the fallthrough logic for options 

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

236 name: {} for name in override_order 

237 } 

238 

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

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

241 

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

243 if section in override_order: 

244 section_items_dict[section][key] = val 

245 

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

247 # Make sure we completely remove empty values again 

248 section_items = { 

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

250 for name in override_order 

251 } 

252 

253 # Yield each group in their override order 

254 for section in override_order: 

255 yield from section_items[section] 

256 

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

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

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

260 options (lists).""" 

261 

262 # Accumulate complex default state. 

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

264 late_eval = set() 

265 # Then set the options with those values 

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

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

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

269 

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

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

272 # commands. 

273 if option is None: 

274 continue 

275 

276 assert option.dest is not None 

277 

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

279 try: 

280 val = strtobool(val) 

281 except ValueError: 

282 self.error( 

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

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

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

286 ) 

287 elif option.action == "count": 

288 with suppress(ValueError): 

289 val = strtobool(val) 

290 with suppress(ValueError): 

291 val = int(val) 

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

293 self.error( 

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

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

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

297 "which is equivalent to 1/0." 

298 ) 

299 elif option.action == "append": 

300 val = val.split() 

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

302 elif option.action == "callback": 

303 assert option.callback is not None 

304 late_eval.add(option.dest) 

305 opt_str = option.get_opt_string() 

306 val = option.convert_value(opt_str, val) 

307 # From take_action 

308 args = option.callback_args or () 

309 kwargs = option.callback_kwargs or {} 

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

311 else: 

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

313 

314 defaults[option.dest] = val 

315 

316 for key in late_eval: 

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

318 self.values = None 

319 return defaults 

320 

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

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

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

324 if not self.process_default_values: 

325 # Old, pre-Optik 1.5 behaviour. 

326 return optparse.Values(self.defaults) 

327 

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

329 try: 

330 self.config.load() 

331 except ConfigurationError as err: 

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

333 

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

335 for option in self._get_all_options(): 

336 assert option.dest is not None 

337 default = defaults.get(option.dest) 

338 if isinstance(default, str): 

339 opt_str = option.get_opt_string() 

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

341 return optparse.Values(defaults) 

342 

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

344 self.print_usage(sys.stderr) 

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

346 

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

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

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

350 no_color = ( 

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

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

353 or "NO_COLOR" in os.environ 

354 ) 

355 console = PipConsole( 

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

357 ) 

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