| 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 |