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.4.3, created at 2024-02-26 06:33 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 06:33 +0000
1"""Base option parser setup"""
3import logging
4import optparse
5import shutil
6import sys
7import textwrap
8from contextlib import suppress
9from typing import Any, Dict, Generator, List, Tuple
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
15logger = logging.getLogger(__name__)
18class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
19 """A prettier/less verbose help formatter for optparse."""
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)
28 def format_option_strings(self, option: optparse.Option) -> str:
29 return self._format_option_strings(option)
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.
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 = []
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)
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()))
55 return "".join(opts)
57 def format_heading(self, heading: str) -> str:
58 if heading == "Options":
59 return ""
60 return heading + ":\n"
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
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 ""
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 ""
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)
100class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter):
101 """Custom help formatter for use in ConfigOptionParser.
103 This is updates the defaults before expanding them, allowing
104 them to show up correctly in the help listing.
106 Also redact auth from url type options
107 """
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)
118 if default_values and option.metavar == "URL":
119 if isinstance(default_values, str):
120 default_values = [default_values]
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 = []
126 for val in default_values:
127 help_text = help_text.replace(val, redact_auth_from_url(val))
129 return help_text
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)
139 self.option_groups.pop()
140 self.option_groups.insert(idx, group)
142 return group
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)
151 return res
154class ConfigOptionParser(CustomOptionParser):
155 """Custom option parser which updates its defaults by checking the
156 configuration files and environmental variables"""
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)
168 assert self.name
169 super().__init__(*args, **kwargs)
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)
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:"]
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
197 section, key = section_key.split(".", 1)
198 if section in override_order:
199 section_items[section].append((key, val))
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
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)."""
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)
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
225 assert option.dest is not None
227 if option.action in ("store_true", "store_false"):
228 try:
229 val = strtobool(val)
230 except ValueError:
231 self.error(
232 f"{val} is not a valid value for {key} option, "
233 "please specify a boolean value like yes/no, "
234 "true/false or 1/0 instead."
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 f"{val} is not a valid value for {key} option, "
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."
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)
263 defaults[option.dest] = val
265 for key in late_eval:
266 defaults[key] = getattr(self.values, key)
267 self.values = None
268 return defaults
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)
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))
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)
292 def error(self, msg: str) -> None:
293 self.print_usage(sys.stderr)
294 self.exit(UNKNOWN_ERROR, f"{msg}\n")