|
|
|
@ -6,7 +6,13 @@ import shutil
|
|
|
|
|
import sys
|
|
|
|
|
import textwrap
|
|
|
|
|
from contextlib import suppress
|
|
|
|
|
from typing import Any, Dict, Generator, List, Tuple
|
|
|
|
|
from typing import IO, Any, Dict, Generator, List, Optional, Tuple
|
|
|
|
|
|
|
|
|
|
from pip._vendor.rich.console import Console, RenderableType, detect_legacy_windows
|
|
|
|
|
from pip._vendor.rich.markup import escape
|
|
|
|
|
from pip._vendor.rich.style import StyleType
|
|
|
|
|
from pip._vendor.rich.text import Text
|
|
|
|
|
from pip._vendor.rich.theme import Theme
|
|
|
|
|
|
|
|
|
|
from pip._internal.cli.status_codes import UNKNOWN_ERROR
|
|
|
|
|
from pip._internal.configuration import Configuration, ConfigurationError
|
|
|
|
@ -18,54 +24,52 @@ logger = logging.getLogger(__name__)
|
|
|
|
|
class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
|
|
|
|
|
"""A prettier/less verbose help formatter for optparse."""
|
|
|
|
|
|
|
|
|
|
styles: Dict[str, StyleType] = {
|
|
|
|
|
"optparse.args": "cyan",
|
|
|
|
|
"optparse.groups": "dark_orange",
|
|
|
|
|
"optparse.help": "default",
|
|
|
|
|
"optparse.metavar": "dark_cyan",
|
|
|
|
|
"optparse.syntax": "bold",
|
|
|
|
|
"optparse.text": "default",
|
|
|
|
|
}
|
|
|
|
|
highlights: List[str] = [
|
|
|
|
|
r"(?:^|\s)(?P<args>-{1,2}[\w]+[\w-]*)", # highlight --words-with-dashes as args
|
|
|
|
|
r"`(?P<syntax>[^`]*)`", # highlight `text in backquotes` as syntax
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
|
|
|
# help position must be aligned with __init__.parseopts.description
|
|
|
|
|
kwargs["max_help_position"] = 30
|
|
|
|
|
kwargs["indent_increment"] = 1
|
|
|
|
|
kwargs["width"] = shutil.get_terminal_size()[0] - 2
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
self.console: Console = Console(theme=Theme(self.styles))
|
|
|
|
|
self.rich_option_strings: Dict[optparse.Option, Text] = {}
|
|
|
|
|
|
|
|
|
|
def format_option_strings(self, option: optparse.Option) -> str:
|
|
|
|
|
return self._format_option_strings(option)
|
|
|
|
|
|
|
|
|
|
def _format_option_strings(
|
|
|
|
|
self, option: optparse.Option, mvarfmt: str = " <{}>", optsep: str = ", "
|
|
|
|
|
) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Return a comma-separated list of option strings and metavars.
|
|
|
|
|
|
|
|
|
|
:param option: tuple of (short opt, long opt), e.g: ('-f', '--format')
|
|
|
|
|
:param mvarfmt: metavar format string
|
|
|
|
|
:param optsep: separator
|
|
|
|
|
"""
|
|
|
|
|
opts = []
|
|
|
|
|
|
|
|
|
|
if option._short_opts:
|
|
|
|
|
opts.append(option._short_opts[0])
|
|
|
|
|
if option._long_opts:
|
|
|
|
|
opts.append(option._long_opts[0])
|
|
|
|
|
if len(opts) > 1:
|
|
|
|
|
opts.insert(1, optsep)
|
|
|
|
|
|
|
|
|
|
if option.takes_value():
|
|
|
|
|
assert option.dest is not None
|
|
|
|
|
metavar = option.metavar or option.dest.lower()
|
|
|
|
|
opts.append(mvarfmt.format(metavar.lower()))
|
|
|
|
|
|
|
|
|
|
return "".join(opts)
|
|
|
|
|
def stringify(self, text: RenderableType) -> str:
|
|
|
|
|
"""Render a rich object as a string."""
|
|
|
|
|
with self.console.capture() as capture:
|
|
|
|
|
self.console.print(text, highlight=False, soft_wrap=True, end="")
|
|
|
|
|
help = capture.get()
|
|
|
|
|
return "\n".join(line.rstrip() for line in help.split("\n"))
|
|
|
|
|
|
|
|
|
|
def format_heading(self, heading: str) -> str:
|
|
|
|
|
if heading == "Options":
|
|
|
|
|
return ""
|
|
|
|
|
return heading + ":\n"
|
|
|
|
|
rich_heading = Text().append(heading, "optparse.groups").append(":\n")
|
|
|
|
|
return self.stringify(rich_heading)
|
|
|
|
|
|
|
|
|
|
def format_usage(self, usage: str) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Ensure there is only one newline between usage and the first heading
|
|
|
|
|
if there is no description.
|
|
|
|
|
"""
|
|
|
|
|
msg = "\nUsage: {}\n".format(self.indent_lines(textwrap.dedent(usage), " "))
|
|
|
|
|
return msg
|
|
|
|
|
rich_usage = (
|
|
|
|
|
Text("\n")
|
|
|
|
|
.append("Usage", "optparse.groups")
|
|
|
|
|
.append(f": {self.indent_lines(textwrap.dedent(usage), ' ')}\n")
|
|
|
|
|
)
|
|
|
|
|
return self.stringify(rich_usage)
|
|
|
|
|
|
|
|
|
|
def format_description(self, description: str) -> str:
|
|
|
|
|
# leave full control over description to us
|
|
|
|
@ -74,13 +78,14 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
|
|
|
|
|
label = "Commands"
|
|
|
|
|
else:
|
|
|
|
|
label = "Description"
|
|
|
|
|
rich_label = self.stringify(Text(label, "optparse.groups"))
|
|
|
|
|
# some doc strings have initial newlines, some don't
|
|
|
|
|
description = description.lstrip("\n")
|
|
|
|
|
# some doc strings have final newlines and spaces, some don't
|
|
|
|
|
description = description.rstrip()
|
|
|
|
|
# dedent, then reindent
|
|
|
|
|
description = self.indent_lines(textwrap.dedent(description), " ")
|
|
|
|
|
description = f"{label}:\n{description}\n"
|
|
|
|
|
description = f"{rich_label}:\n{description}\n"
|
|
|
|
|
return description
|
|
|
|
|
else:
|
|
|
|
|
return ""
|
|
|
|
@ -88,10 +93,95 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
|
|
|
|
|
def format_epilog(self, epilog: str) -> str:
|
|
|
|
|
# leave full control over epilog to us
|
|
|
|
|
if epilog:
|
|
|
|
|
return epilog
|
|
|
|
|
rich_epilog = Text(epilog, style="optparse.text")
|
|
|
|
|
return self.stringify(rich_epilog)
|
|
|
|
|
else:
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
def rich_expand_default(self, option: optparse.Option) -> Text:
|
|
|
|
|
# `HelpFormatter.expand_default()` equivalent that returns a `Text`.
|
|
|
|
|
assert option.help is not None
|
|
|
|
|
if self.parser is None or not self.default_tag:
|
|
|
|
|
help = option.help
|
|
|
|
|
else:
|
|
|
|
|
default_value = self.parser.defaults.get(option.dest) # type: ignore
|
|
|
|
|
if default_value is optparse.NO_DEFAULT or default_value is None:
|
|
|
|
|
default_value = self.NO_DEFAULT_VALUE
|
|
|
|
|
help = option.help.replace(self.default_tag, escape(str(default_value)))
|
|
|
|
|
rich_help = Text.from_markup(help, style="optparse.help")
|
|
|
|
|
for highlight in self.highlights:
|
|
|
|
|
rich_help.highlight_regex(highlight, style_prefix="optparse.")
|
|
|
|
|
return rich_help
|
|
|
|
|
|
|
|
|
|
def format_option(self, option: optparse.Option) -> str:
|
|
|
|
|
# Overridden to call the rich methods.
|
|
|
|
|
result: List[Text] = []
|
|
|
|
|
opts = self.rich_option_strings[option]
|
|
|
|
|
opt_width = self.help_position - self.current_indent - 2
|
|
|
|
|
if len(opts) > opt_width:
|
|
|
|
|
opts.append("\n")
|
|
|
|
|
indent_first = self.help_position
|
|
|
|
|
else: # start help on same line as opts
|
|
|
|
|
opts.set_length(opt_width + 2)
|
|
|
|
|
indent_first = 0
|
|
|
|
|
opts.pad_left(self.current_indent)
|
|
|
|
|
result.append(opts)
|
|
|
|
|
if option.help:
|
|
|
|
|
help_text = self.rich_expand_default(option)
|
|
|
|
|
help_text.expand_tabs(8) # textwrap expands tabs first
|
|
|
|
|
help_text.plain = help_text.plain.translate(
|
|
|
|
|
textwrap.TextWrapper.unicode_whitespace_trans
|
|
|
|
|
) # textwrap converts whitespace to " " second
|
|
|
|
|
help_lines = help_text.wrap(self.console, self.help_width)
|
|
|
|
|
result.append(Text(" " * indent_first) + help_lines[0] + "\n")
|
|
|
|
|
indent = Text(" " * self.help_position)
|
|
|
|
|
for line in help_lines[1:]:
|
|
|
|
|
result.append(indent + line + "\n")
|
|
|
|
|
elif opts.plain[-1] != "\n":
|
|
|
|
|
result.append(Text("\n"))
|
|
|
|
|
else:
|
|
|
|
|
pass # pragma: no cover
|
|
|
|
|
return self.stringify(Text().join(result))
|
|
|
|
|
|
|
|
|
|
def store_option_strings(self, parser: optparse.OptionParser) -> None:
|
|
|
|
|
# Overridden to call the rich methods.
|
|
|
|
|
self.indent()
|
|
|
|
|
max_len = 0
|
|
|
|
|
for opt in parser.option_list:
|
|
|
|
|
strings = self.rich_format_option_strings(opt)
|
|
|
|
|
self.option_strings[opt] = strings.plain
|
|
|
|
|
self.rich_option_strings[opt] = strings
|
|
|
|
|
max_len = max(max_len, len(strings) + self.current_indent)
|
|
|
|
|
self.indent()
|
|
|
|
|
for group in parser.option_groups:
|
|
|
|
|
for opt in group.option_list:
|
|
|
|
|
strings = self.rich_format_option_strings(opt)
|
|
|
|
|
self.option_strings[opt] = strings.plain
|
|
|
|
|
self.rich_option_strings[opt] = strings
|
|
|
|
|
max_len = max(max_len, len(strings) + self.current_indent)
|
|
|
|
|
self.dedent()
|
|
|
|
|
self.dedent()
|
|
|
|
|
self.help_position = min(max_len + 2, self.max_help_position)
|
|
|
|
|
self.help_width = max(self.width - self.help_position, 11)
|
|
|
|
|
|
|
|
|
|
def rich_format_option_strings(self, option: optparse.Option) -> Text:
|
|
|
|
|
# `HelpFormatter.format_option_strings()` equivalent that returns a `Text`.
|
|
|
|
|
opts: List[Text] = []
|
|
|
|
|
|
|
|
|
|
if option._short_opts:
|
|
|
|
|
opts.append(Text(option._short_opts[0], "optparse.args"))
|
|
|
|
|
if option._long_opts:
|
|
|
|
|
opts.append(Text(option._long_opts[0], "optparse.args"))
|
|
|
|
|
if len(opts) > 1:
|
|
|
|
|
opts.insert(1, Text(", "))
|
|
|
|
|
|
|
|
|
|
if option.takes_value():
|
|
|
|
|
assert option.dest is not None
|
|
|
|
|
metavar = option.metavar or option.dest.lower()
|
|
|
|
|
opts.append(Text(" ").append(f"<{metavar.lower()}>", "optparse.metavar"))
|
|
|
|
|
|
|
|
|
|
return Text().join(opts)
|
|
|
|
|
|
|
|
|
|
def indent_lines(self, text: str, indent: str) -> str:
|
|
|
|
|
new_lines = [indent + line for line in text.split("\n")]
|
|
|
|
|
return "\n".join(new_lines)
|
|
|
|
@ -106,14 +196,14 @@ class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter):
|
|
|
|
|
Also redact auth from url type options
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def expand_default(self, option: optparse.Option) -> str:
|
|
|
|
|
def rich_expand_default(self, option: optparse.Option) -> Text:
|
|
|
|
|
default_values = None
|
|
|
|
|
if self.parser is not None:
|
|
|
|
|
assert isinstance(self.parser, ConfigOptionParser)
|
|
|
|
|
self.parser._update_defaults(self.parser.defaults)
|
|
|
|
|
assert option.dest is not None
|
|
|
|
|
default_values = self.parser.defaults.get(option.dest)
|
|
|
|
|
help_text = super().expand_default(option)
|
|
|
|
|
help_text = super().rich_expand_default(option)
|
|
|
|
|
|
|
|
|
|
if default_values and option.metavar == "URL":
|
|
|
|
|
if isinstance(default_values, str):
|
|
|
|
@ -124,7 +214,8 @@ class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter):
|
|
|
|
|
default_values = []
|
|
|
|
|
|
|
|
|
|
for val in default_values:
|
|
|
|
|
help_text = help_text.replace(val, redact_auth_from_url(val))
|
|
|
|
|
new_val = escape(redact_auth_from_url(val))
|
|
|
|
|
help_text = Text(new_val).join(help_text.split(val))
|
|
|
|
|
|
|
|
|
|
return help_text
|
|
|
|
|
|
|
|
|
@ -150,6 +241,22 @@ class CustomOptionParser(optparse.OptionParser):
|
|
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
def _print_ansi(self, text: str, file: Optional[IO[str]] = None) -> None:
|
|
|
|
|
if file is None:
|
|
|
|
|
file = sys.stdout
|
|
|
|
|
if detect_legacy_windows():
|
|
|
|
|
console = Console(file=file)
|
|
|
|
|
console.print(Text.from_ansi(text), soft_wrap=True)
|
|
|
|
|
else:
|
|
|
|
|
file.write(text)
|
|
|
|
|
|
|
|
|
|
def print_usage(self, file: Optional[IO[str]] = None) -> None:
|
|
|
|
|
if self.usage:
|
|
|
|
|
self._print_ansi(self.get_usage(), file=file)
|
|
|
|
|
|
|
|
|
|
def print_help(self, file: Optional[IO[str]] = None) -> None:
|
|
|
|
|
self._print_ansi(self.format_help(), file=file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ConfigOptionParser(CustomOptionParser):
|
|
|
|
|
"""Custom option parser which updates its defaults by checking the
|
|
|
|
|