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

166 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:48 +0000

1"""Base option parser setup""" 

2 

3import logging 

4import optparse 

5import shutil 

6import sys 

7import textwrap 

8from contextlib import suppress 

9from typing import Any, Dict, Generator, List, Tuple 

10 

11from pip._internal.cli.status_codes import UNKNOWN_ERROR 

12from pip._internal.configuration import Configuration, ConfigurationError 

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

14 

15logger = logging.getLogger(__name__) 

16 

17 

18class PrettyHelpFormatter(optparse.IndentedHelpFormatter): 

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

20 

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

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

23 kwargs["max_help_position"] = 30 

24 kwargs["indent_increment"] = 1 

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

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

27 

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

29 return self._format_option_strings(option) 

30 

31 def _format_option_strings( 

32 self, option: optparse.Option, mvarfmt: str = " <{}>", optsep: str = ", " 

33 ) -> str: 

34 """ 

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

36 

37 :param option: tuple of (short opt, long opt), e.g: ('-f', '--format') 

38 :param mvarfmt: metavar format string 

39 :param optsep: separator 

40 """ 

41 opts = [] 

42 

43 if option._short_opts: 

44 opts.append(option._short_opts[0]) 

45 if option._long_opts: 

46 opts.append(option._long_opts[0]) 

47 if len(opts) > 1: 

48 opts.insert(1, optsep) 

49 

50 if option.takes_value(): 

51 assert option.dest is not None 

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

53 opts.append(mvarfmt.format(metavar.lower())) 

54 

55 return "".join(opts) 

56 

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

58 if heading == "Options": 

59 return "" 

60 return heading + ":\n" 

61 

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

63 """ 

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

65 if there is no description. 

66 """ 

67 msg = "\nUsage: {}\n".format(self.indent_lines(textwrap.dedent(usage), " ")) 

68 return msg 

69 

70 def format_description(self, description: str) -> str: 

71 # leave full control over description to us 

72 if description: 

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

74 label = "Commands" 

75 else: 

76 label = "Description" 

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

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

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

80 description = description.rstrip() 

81 # dedent, then reindent 

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

83 description = f"{label}:\n{description}\n" 

84 return description 

85 else: 

86 return "" 

87 

88 def format_epilog(self, epilog: str) -> str: 

89 # leave full control over epilog to us 

90 if epilog: 

91 return epilog 

92 else: 

93 return "" 

94 

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

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

97 return "\n".join(new_lines) 

98 

99 

100class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter): 

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

102 

103 This is updates the defaults before expanding them, allowing 

104 them to show up correctly in the help listing. 

105 

106 Also redact auth from url type options 

107 """ 

108 

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

110 default_values = None 

111 if self.parser is not None: 

112 assert isinstance(self.parser, ConfigOptionParser) 

113 self.parser._update_defaults(self.parser.defaults) 

114 assert option.dest is not None 

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

116 help_text = super().expand_default(option) 

117 

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

119 if isinstance(default_values, str): 

120 default_values = [default_values] 

121 

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

123 if not isinstance(default_values, list): 

124 default_values = [] 

125 

126 for val in default_values: 

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

128 

129 return help_text 

130 

131 

132class CustomOptionParser(optparse.OptionParser): 

133 def insert_option_group( 

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

135 ) -> optparse.OptionGroup: 

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

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

138 

139 self.option_groups.pop() 

140 self.option_groups.insert(idx, group) 

141 

142 return group 

143 

144 @property 

145 def option_list_all(self) -> List[optparse.Option]: 

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

147 res = self.option_list[:] 

148 for i in self.option_groups: 

149 res.extend(i.option_list) 

150 

151 return res 

152 

153 

154class ConfigOptionParser(CustomOptionParser): 

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

156 configuration files and environmental variables""" 

157 

158 def __init__( 

159 self, 

160 *args: Any, 

161 name: str, 

162 isolated: bool = False, 

163 **kwargs: Any, 

164 ) -> None: 

165 self.name = name 

166 self.config = Configuration(isolated) 

167 

168 assert self.name 

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

170 

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

172 try: 

173 return option.check_value(key, val) 

174 except optparse.OptionValueError as exc: 

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

176 sys.exit(3) 

177 

178 def _get_ordered_configuration_items( 

179 self, 

180 ) -> Generator[Tuple[str, Any], None, None]: 

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

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

183 

184 # Pool the options into different groups 

185 section_items: Dict[str, List[Tuple[str, Any]]] = { 

186 name: [] for name in override_order 

187 } 

188 for section_key, val in self.config.items(): 

189 # ignore empty values 

190 if not val: 

191 logger.debug( 

192 "Ignoring configuration key '%s' as it's value is empty.", 

193 section_key, 

194 ) 

195 continue 

196 

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

198 if section in override_order: 

199 section_items[section].append((key, val)) 

200 

201 # Yield each group in their override order 

202 for section in override_order: 

203 for key, val in section_items[section]: 

204 yield key, val 

205 

206 def _update_defaults(self, defaults: Dict[str, Any]) -> Dict[str, Any]: 

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

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

209 options (lists).""" 

210 

211 # Accumulate complex default state. 

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

213 late_eval = set() 

214 # Then set the options with those values 

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

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

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

218 

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

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

221 # commands. 

222 if option is None: 

223 continue 

224 

225 assert option.dest is not None 

226 

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

228 try: 

229 val = strtobool(val) 

230 except ValueError: 

231 self.error( 

232 "{} is not a valid value for {} option, " # noqa 

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

234 "true/false or 1/0 instead.".format(val, key) 

235 ) 

236 elif option.action == "count": 

237 with suppress(ValueError): 

238 val = strtobool(val) 

239 with suppress(ValueError): 

240 val = int(val) 

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

242 self.error( 

243 "{} is not a valid value for {} option, " # noqa 

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

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

246 "which is equivalent to 1/0.".format(val, key) 

247 ) 

248 elif option.action == "append": 

249 val = val.split() 

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

251 elif option.action == "callback": 

252 assert option.callback is not None 

253 late_eval.add(option.dest) 

254 opt_str = option.get_opt_string() 

255 val = option.convert_value(opt_str, val) 

256 # From take_action 

257 args = option.callback_args or () 

258 kwargs = option.callback_kwargs or {} 

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

260 else: 

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

262 

263 defaults[option.dest] = val 

264 

265 for key in late_eval: 

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

267 self.values = None 

268 return defaults 

269 

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

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

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

273 if not self.process_default_values: 

274 # Old, pre-Optik 1.5 behaviour. 

275 return optparse.Values(self.defaults) 

276 

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

278 try: 

279 self.config.load() 

280 except ConfigurationError as err: 

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

282 

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

284 for option in self._get_all_options(): 

285 assert option.dest is not None 

286 default = defaults.get(option.dest) 

287 if isinstance(default, str): 

288 opt_str = option.get_opt_string() 

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

290 return optparse.Values(defaults) 

291 

292 def error(self, msg: str) -> None: 

293 self.print_usage(sys.stderr) 

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