123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- import typing as t
- from contextlib import contextmanager
- from gettext import gettext as _
- from ._compat import term_len
- from .parser import split_opt
- # Can force a width. This is used by the test system
- FORCED_WIDTH: t.Optional[int] = None
- def measure_table(rows: t.Iterable[t.Tuple[str, str]]) -> t.Tuple[int, ...]:
- widths: t.Dict[int, int] = {}
- for row in rows:
- for idx, col in enumerate(row):
- widths[idx] = max(widths.get(idx, 0), term_len(col))
- return tuple(y for x, y in sorted(widths.items()))
- def iter_rows(
- rows: t.Iterable[t.Tuple[str, str]], col_count: int
- ) -> t.Iterator[t.Tuple[str, ...]]:
- for row in rows:
- yield row + ("",) * (col_count - len(row))
- def wrap_text(
- text: str,
- width: int = 78,
- initial_indent: str = "",
- subsequent_indent: str = "",
- preserve_paragraphs: bool = False,
- ) -> str:
- """A helper function that intelligently wraps text. By default, it
- assumes that it operates on a single paragraph of text but if the
- `preserve_paragraphs` parameter is provided it will intelligently
- handle paragraphs (defined by two empty lines).
- If paragraphs are handled, a paragraph can be prefixed with an empty
- line containing the ``\\b`` character (``\\x08``) to indicate that
- no rewrapping should happen in that block.
- :param text: the text that should be rewrapped.
- :param width: the maximum width for the text.
- :param initial_indent: the initial indent that should be placed on the
- first line as a string.
- :param subsequent_indent: the indent string that should be placed on
- each consecutive line.
- :param preserve_paragraphs: if this flag is set then the wrapping will
- intelligently handle paragraphs.
- """
- from ._textwrap import TextWrapper
- text = text.expandtabs()
- wrapper = TextWrapper(
- width,
- initial_indent=initial_indent,
- subsequent_indent=subsequent_indent,
- replace_whitespace=False,
- )
- if not preserve_paragraphs:
- return wrapper.fill(text)
- p: t.List[t.Tuple[int, bool, str]] = []
- buf: t.List[str] = []
- indent = None
- def _flush_par() -> None:
- if not buf:
- return
- if buf[0].strip() == "\b":
- p.append((indent or 0, True, "\n".join(buf[1:])))
- else:
- p.append((indent or 0, False, " ".join(buf)))
- del buf[:]
- for line in text.splitlines():
- if not line:
- _flush_par()
- indent = None
- else:
- if indent is None:
- orig_len = term_len(line)
- line = line.lstrip()
- indent = orig_len - term_len(line)
- buf.append(line)
- _flush_par()
- rv = []
- for indent, raw, text in p:
- with wrapper.extra_indent(" " * indent):
- if raw:
- rv.append(wrapper.indent_only(text))
- else:
- rv.append(wrapper.fill(text))
- return "\n\n".join(rv)
- class HelpFormatter:
- """This class helps with formatting text-based help pages. It's
- usually just needed for very special internal cases, but it's also
- exposed so that developers can write their own fancy outputs.
- At present, it always writes into memory.
- :param indent_increment: the additional increment for each level.
- :param width: the width for the text. This defaults to the terminal
- width clamped to a maximum of 78.
- """
- def __init__(
- self,
- indent_increment: int = 2,
- width: t.Optional[int] = None,
- max_width: t.Optional[int] = None,
- ) -> None:
- import shutil
- self.indent_increment = indent_increment
- if max_width is None:
- max_width = 80
- if width is None:
- width = FORCED_WIDTH
- if width is None:
- width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50)
- self.width = width
- self.current_indent = 0
- self.buffer: t.List[str] = []
- def write(self, string: str) -> None:
- """Writes a unicode string into the internal buffer."""
- self.buffer.append(string)
- def indent(self) -> None:
- """Increases the indentation."""
- self.current_indent += self.indent_increment
- def dedent(self) -> None:
- """Decreases the indentation."""
- self.current_indent -= self.indent_increment
- def write_usage(
- self, prog: str, args: str = "", prefix: t.Optional[str] = None
- ) -> None:
- """Writes a usage line into the buffer.
- :param prog: the program name.
- :param args: whitespace separated list of arguments.
- :param prefix: The prefix for the first line. Defaults to
- ``"Usage: "``.
- """
- if prefix is None:
- prefix = f"{_('Usage:')} "
- usage_prefix = f"{prefix:>{self.current_indent}}{prog} "
- text_width = self.width - self.current_indent
- if text_width >= (term_len(usage_prefix) + 20):
- # The arguments will fit to the right of the prefix.
- indent = " " * term_len(usage_prefix)
- self.write(
- wrap_text(
- args,
- text_width,
- initial_indent=usage_prefix,
- subsequent_indent=indent,
- )
- )
- else:
- # The prefix is too long, put the arguments on the next line.
- self.write(usage_prefix)
- self.write("\n")
- indent = " " * (max(self.current_indent, term_len(prefix)) + 4)
- self.write(
- wrap_text(
- args, text_width, initial_indent=indent, subsequent_indent=indent
- )
- )
- self.write("\n")
- def write_heading(self, heading: str) -> None:
- """Writes a heading into the buffer."""
- self.write(f"{'':>{self.current_indent}}{heading}:\n")
- def write_paragraph(self) -> None:
- """Writes a paragraph into the buffer."""
- if self.buffer:
- self.write("\n")
- def write_text(self, text: str) -> None:
- """Writes re-indented text into the buffer. This rewraps and
- preserves paragraphs.
- """
- indent = " " * self.current_indent
- self.write(
- wrap_text(
- text,
- self.width,
- initial_indent=indent,
- subsequent_indent=indent,
- preserve_paragraphs=True,
- )
- )
- self.write("\n")
- def write_dl(
- self,
- rows: t.Sequence[t.Tuple[str, str]],
- col_max: int = 30,
- col_spacing: int = 2,
- ) -> None:
- """Writes a definition list into the buffer. This is how options
- and commands are usually formatted.
- :param rows: a list of two item tuples for the terms and values.
- :param col_max: the maximum width of the first column.
- :param col_spacing: the number of spaces between the first and
- second column.
- """
- rows = list(rows)
- widths = measure_table(rows)
- if len(widths) != 2:
- raise TypeError("Expected two columns for definition list")
- first_col = min(widths[0], col_max) + col_spacing
- for first, second in iter_rows(rows, len(widths)):
- self.write(f"{'':>{self.current_indent}}{first}")
- if not second:
- self.write("\n")
- continue
- if term_len(first) <= first_col - col_spacing:
- self.write(" " * (first_col - term_len(first)))
- else:
- self.write("\n")
- self.write(" " * (first_col + self.current_indent))
- text_width = max(self.width - first_col - 2, 10)
- wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True)
- lines = wrapped_text.splitlines()
- if lines:
- self.write(f"{lines[0]}\n")
- for line in lines[1:]:
- self.write(f"{'':>{first_col + self.current_indent}}{line}\n")
- else:
- self.write("\n")
- @contextmanager
- def section(self, name: str) -> t.Iterator[None]:
- """Helpful context manager that writes a paragraph, a heading,
- and the indents.
- :param name: the section name that is written as heading.
- """
- self.write_paragraph()
- self.write_heading(name)
- self.indent()
- try:
- yield
- finally:
- self.dedent()
- @contextmanager
- def indentation(self) -> t.Iterator[None]:
- """A context manager that increases the indentation."""
- self.indent()
- try:
- yield
- finally:
- self.dedent()
- def getvalue(self) -> str:
- """Returns the buffer contents."""
- return "".join(self.buffer)
- def join_options(options: t.Sequence[str]) -> t.Tuple[str, bool]:
- """Given a list of option strings this joins them in the most appropriate
- way and returns them in the form ``(formatted_string,
- any_prefix_is_slash)`` where the second item in the tuple is a flag that
- indicates if any of the option prefixes was a slash.
- """
- rv = []
- any_prefix_is_slash = False
- for opt in options:
- prefix = split_opt(opt)[0]
- if prefix == "/":
- any_prefix_is_slash = True
- rv.append((len(prefix), opt))
- rv.sort(key=lambda x: x[0])
- return ", ".join(x[1] for x in rv), any_prefix_is_slash
|