python (3.12.0)
1 """Base option parser setup"""
2
3 import logging
4 import optparse
5 import shutil
6 import sys
7 import textwrap
8 from contextlib import suppress
9 from typing import Any, Dict, Generator, List, Tuple
10
11 from pip._internal.cli.status_codes import UNKNOWN_ERROR
12 from pip._internal.configuration import Configuration, ConfigurationError
13 from pip._internal.utils.misc import redact_auth_from_url, strtobool
14
15 logger = logging.getLogger(__name__)
16
17
18 class ESC[4;38;5;81mPrettyHelpFormatter(ESC[4;38;5;149moptparseESC[4;38;5;149m.ESC[4;38;5;149mIndentedHelpFormatter):
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
100 class ESC[4;38;5;81mUpdatingDefaultsHelpFormatter(ESC[4;38;5;149mPrettyHelpFormatter):
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
132 class ESC[4;38;5;81mCustomOptionParser(ESC[4;38;5;149moptparseESC[4;38;5;149m.ESC[4;38;5;149mOptionParser):
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
154 class ESC[4;38;5;81mConfigOptionParser(ESC[4;38;5;149mCustomOptionParser):
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")