summaryrefslogtreecommitdiff
path: root/src/click/formatting.py
diff options
context:
space:
mode:
authorDavid Lord <davidism@gmail.com>2020-03-06 12:45:31 -0800
committerDavid Lord <davidism@gmail.com>2020-03-06 12:45:31 -0800
commit079f91a2396c5c9264f34f95894259b7b15eb89c (patch)
treeca0eb6996ecf1e388f2aa3f01b3bff88a1d7f411 /src/click/formatting.py
parent3d1dcc28e115507f1eafbdb75465223c5da58205 (diff)
downloadclick-079f91a2396c5c9264f34f95894259b7b15eb89c.tar.gz
move to src directory
Diffstat (limited to 'src/click/formatting.py')
-rw-r--r--src/click/formatting.py262
1 files changed, 262 insertions, 0 deletions
diff --git a/src/click/formatting.py b/src/click/formatting.py
new file mode 100644
index 0000000..4ae5f07
--- /dev/null
+++ b/src/click/formatting.py
@@ -0,0 +1,262 @@
+from contextlib import contextmanager
+from .termui import get_terminal_size
+from .parser import split_opt
+from ._compat import term_len
+
+
+# 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 = '%*s%s ' % (self.current_indent, prefix, 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):
+ """Writes a heading into the buffer."""
+ self.write('%*s%s:\n' % (self.current_indent, '', heading))
+
+ 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('%*s%s' % (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(lines[0] + '\n')
+
+ for line in lines[1:]:
+ self.write('%*s%s\n' % (first_col + self.current_indent, '', line))
+
+ 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