123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- from contextlib import contextmanager
- from ._compat import term_len
- from .parser import split_opt
- from .termui import get_terminal_size
- # Can force a width. This is used by the test system
- FORCED_WIDTH = None
- def measure_table(rows):
- widths = {}
- 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, col_count):
- for row in rows:
- row = tuple(row)
- yield row + ("",) * (col_count - len(row))
- def wrap_text(
- text, width=78, initial_indent="", subsequent_indent="", preserve_paragraphs=False
- ):
- """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 = []
- buf = []
- indent = None
- def _flush_par():
- 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(object):
- """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=2, width=None, max_width=None):
- 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(get_terminal_size()[0], max_width) - 2, 50)
- self.width = width
- self.current_indent = 0
- self.buffer = []
- def write(self, string):
- """Writes a unicode string into the internal buffer."""
- self.buffer.append(string)
- def indent(self):
- """Increases the indentation."""
- self.current_indent += self.indent_increment
- def dedent(self):
- """Decreases the indentation."""
- self.current_indent -= self.indent_increment
- def write_usage(self, prog, args="", prefix="Usage: "):
- """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.
- """
- usage_prefix = "{:>{w}}{} ".format(prefix, prog, w=self.current_indent)
- 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):
- """Writes a heading into the buffer."""
- self.write("{:>{w}}{}:\n".format("", heading, w=self.current_indent))
- def write_paragraph(self):
- """Writes a paragraph into the buffer."""
- if self.buffer:
- self.write("\n")
- def write_text(self, text):
- """Writes re-indented text into the buffer. This rewraps and
- preserves paragraphs.
- """
- text_width = max(self.width - self.current_indent, 11)
- indent = " " * self.current_indent
- self.write(
- wrap_text(
- text,
- text_width,
- initial_indent=indent,
- subsequent_indent=indent,
- preserve_paragraphs=True,
- )
- )
- self.write("\n")
- def write_dl(self, rows, col_max=30, col_spacing=2):
- """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("{:>{w}}{}".format("", first, w=self.current_indent))
- 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("{}\n".format(lines[0]))
- for line in lines[1:]:
- self.write(
- "{:>{w}}{}\n".format(
- "", line, w=first_col + self.current_indent
- )
- )
- if len(lines) > 1:
- # separate long help from next option
- self.write("\n")
- else:
- self.write("\n")
- @contextmanager
- def section(self, name):
- """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):
- """A context manager that increases the indentation."""
- self.indent()
- try:
- yield
- finally:
- self.dedent()
- def getvalue(self):
- """Returns the buffer contents."""
- return "".join(self.buffer)
- def join_options(options):
- """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])
- rv = ", ".join(x[1] for x in rv)
- return rv, any_prefix_is_slash
|