formatting.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. from __future__ import annotations
  2. import collections.abc as cabc
  3. from contextlib import contextmanager
  4. from gettext import gettext as _
  5. from ._compat import term_len
  6. from .parser import _split_opt
  7. # Can force a width. This is used by the test system
  8. FORCED_WIDTH: int | None = None
  9. def measure_table(rows: cabc.Iterable[tuple[str, str]]) -> tuple[int, ...]:
  10. widths: dict[int, int] = {}
  11. for row in rows:
  12. for idx, col in enumerate(row):
  13. widths[idx] = max(widths.get(idx, 0), term_len(col))
  14. return tuple(y for x, y in sorted(widths.items()))
  15. def iter_rows(
  16. rows: cabc.Iterable[tuple[str, str]], col_count: int
  17. ) -> cabc.Iterator[tuple[str, ...]]:
  18. for row in rows:
  19. yield row + ("",) * (col_count - len(row))
  20. def wrap_text(
  21. text: str,
  22. width: int = 78,
  23. initial_indent: str = "",
  24. subsequent_indent: str = "",
  25. preserve_paragraphs: bool = False,
  26. ) -> str:
  27. """A helper function that intelligently wraps text. By default, it
  28. assumes that it operates on a single paragraph of text but if the
  29. `preserve_paragraphs` parameter is provided it will intelligently
  30. handle paragraphs (defined by two empty lines).
  31. If paragraphs are handled, a paragraph can be prefixed with an empty
  32. line containing the ``\\b`` character (``\\x08``) to indicate that
  33. no rewrapping should happen in that block.
  34. :param text: the text that should be rewrapped.
  35. :param width: the maximum width for the text.
  36. :param initial_indent: the initial indent that should be placed on the
  37. first line as a string.
  38. :param subsequent_indent: the indent string that should be placed on
  39. each consecutive line.
  40. :param preserve_paragraphs: if this flag is set then the wrapping will
  41. intelligently handle paragraphs.
  42. .. versionchanged:: 8.4.0
  43. Width is measured in visible characters. ANSI escape sequences in
  44. ``text``, ``initial_indent``, or ``subsequent_indent`` no longer
  45. count toward the width budget, so styled input wraps based on what
  46. the user sees instead of raw byte length.
  47. """
  48. from ._textwrap import TextWrapper
  49. text = text.expandtabs()
  50. wrapper = TextWrapper(
  51. width,
  52. initial_indent=initial_indent,
  53. subsequent_indent=subsequent_indent,
  54. replace_whitespace=False,
  55. )
  56. if not preserve_paragraphs:
  57. return wrapper.fill(text)
  58. p: list[tuple[int, bool, str]] = []
  59. buf: list[str] = []
  60. indent = None
  61. def _flush_par() -> None:
  62. if not buf:
  63. return
  64. if buf[0].strip() == "\b":
  65. p.append((indent or 0, True, "\n".join(buf[1:])))
  66. else:
  67. p.append((indent or 0, False, " ".join(buf)))
  68. del buf[:]
  69. for line in text.splitlines():
  70. if not line:
  71. _flush_par()
  72. indent = None
  73. else:
  74. if indent is None:
  75. orig_len = term_len(line)
  76. line = line.lstrip()
  77. indent = orig_len - term_len(line)
  78. buf.append(line)
  79. _flush_par()
  80. rv = []
  81. for indent, raw, text in p:
  82. with wrapper.extra_indent(" " * indent):
  83. if raw:
  84. rv.append(wrapper.indent_only(text))
  85. else:
  86. rv.append(wrapper.fill(text))
  87. return "\n\n".join(rv)
  88. class HelpFormatter:
  89. """This class helps with formatting text-based help pages. It's
  90. usually just needed for very special internal cases, but it's also
  91. exposed so that developers can write their own fancy outputs.
  92. At present, it always writes into memory.
  93. :param indent_increment: the additional increment for each level.
  94. :param width: the width for the text. This defaults to the terminal
  95. width clamped to a maximum of 78.
  96. """
  97. indent_increment: int
  98. width: int
  99. current_indent: int
  100. buffer: list[str]
  101. def __init__(
  102. self,
  103. indent_increment: int = 2,
  104. width: int | None = None,
  105. max_width: int | None = None,
  106. ) -> None:
  107. self.indent_increment = indent_increment
  108. if max_width is None:
  109. max_width = 80
  110. if width is None:
  111. import shutil
  112. width = FORCED_WIDTH
  113. if width is None:
  114. width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50)
  115. self.width = width
  116. self.current_indent = 0
  117. self.buffer = []
  118. def write(self, string: str) -> None:
  119. """Writes a unicode string into the internal buffer."""
  120. self.buffer.append(string)
  121. def indent(self) -> None:
  122. """Increases the indentation."""
  123. self.current_indent += self.indent_increment
  124. def dedent(self) -> None:
  125. """Decreases the indentation."""
  126. self.current_indent -= self.indent_increment
  127. def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> None:
  128. """Writes a usage line into the buffer.
  129. :param prog: the program name.
  130. :param args: whitespace separated list of arguments.
  131. :param prefix: The prefix for the first line. Defaults to
  132. ``"Usage: "``.
  133. """
  134. if prefix is None:
  135. prefix = "{usage} ".format(usage=_("Usage:"))
  136. usage_prefix = f"{prefix:>{self.current_indent}}{prog} "
  137. text_width = self.width - self.current_indent
  138. if not args:
  139. # Without args, the prefix's trailing space and the wrap_text
  140. # call that would normally place args on the line are both
  141. # unnecessary. Emit just the prefix line.
  142. self.write(usage_prefix.rstrip(" "))
  143. self.write("\n")
  144. return
  145. if text_width >= (term_len(usage_prefix) + 20):
  146. # The arguments will fit to the right of the prefix.
  147. indent = " " * term_len(usage_prefix)
  148. self.write(
  149. wrap_text(
  150. args,
  151. text_width,
  152. initial_indent=usage_prefix,
  153. subsequent_indent=indent,
  154. )
  155. )
  156. else:
  157. # The prefix is too long, put the arguments on the next line.
  158. self.write(usage_prefix)
  159. self.write("\n")
  160. indent = " " * (max(self.current_indent, term_len(prefix)) + 4)
  161. self.write(
  162. wrap_text(
  163. args, text_width, initial_indent=indent, subsequent_indent=indent
  164. )
  165. )
  166. self.write("\n")
  167. def write_heading(self, heading: str) -> None:
  168. """Writes a heading into the buffer."""
  169. self.write(f"{'':>{self.current_indent}}{heading}:\n")
  170. def write_paragraph(self) -> None:
  171. """Writes a paragraph into the buffer."""
  172. if self.buffer:
  173. self.write("\n")
  174. def write_text(self, text: str) -> None:
  175. """Writes re-indented text into the buffer. This rewraps and
  176. preserves paragraphs.
  177. """
  178. indent = " " * self.current_indent
  179. self.write(
  180. wrap_text(
  181. text,
  182. self.width,
  183. initial_indent=indent,
  184. subsequent_indent=indent,
  185. preserve_paragraphs=True,
  186. )
  187. )
  188. self.write("\n")
  189. def write_dl(
  190. self,
  191. rows: cabc.Iterable[tuple[str, str]],
  192. col_max: int = 30,
  193. col_spacing: int = 2,
  194. ) -> None:
  195. """Writes a definition list into the buffer. This is how options
  196. and commands are usually formatted.
  197. :param rows: a list of two item tuples for the terms and values.
  198. :param col_max: the maximum width of the first column.
  199. :param col_spacing: the number of spaces between the first and
  200. second column.
  201. """
  202. rows = list(rows)
  203. widths = measure_table(rows)
  204. if len(widths) != 2:
  205. raise TypeError("Expected two columns for definition list")
  206. first_col = min(widths[0], col_max) + col_spacing
  207. for first, second in iter_rows(rows, len(widths)):
  208. self.write(f"{'':>{self.current_indent}}{first}")
  209. if not second:
  210. self.write("\n")
  211. continue
  212. if term_len(first) <= first_col - col_spacing:
  213. self.write(" " * (first_col - term_len(first)))
  214. else:
  215. self.write("\n")
  216. self.write(" " * (first_col + self.current_indent))
  217. text_width = max(self.width - first_col - 2, 10)
  218. wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True)
  219. lines = wrapped_text.splitlines()
  220. if lines:
  221. self.write(f"{lines[0]}\n")
  222. for line in lines[1:]:
  223. self.write(f"{'':>{first_col + self.current_indent}}{line}\n")
  224. else:
  225. self.write("\n")
  226. @contextmanager
  227. def section(self, name: str) -> cabc.Generator[None]:
  228. """Helpful context manager that writes a paragraph, a heading,
  229. and the indents.
  230. :param name: the section name that is written as heading.
  231. """
  232. self.write_paragraph()
  233. self.write_heading(name)
  234. self.indent()
  235. try:
  236. yield
  237. finally:
  238. self.dedent()
  239. @contextmanager
  240. def indentation(self) -> cabc.Generator[None]:
  241. """A context manager that increases the indentation."""
  242. self.indent()
  243. try:
  244. yield
  245. finally:
  246. self.dedent()
  247. def getvalue(self) -> str:
  248. """Returns the buffer contents."""
  249. return "".join(self.buffer)
  250. def join_options(options: cabc.Iterable[str]) -> tuple[str, bool]:
  251. """Given a list of option strings this joins them in the most appropriate
  252. way and returns them in the form ``(formatted_string,
  253. any_prefix_is_slash)`` where the second item in the tuple is a flag that
  254. indicates if any of the option prefixes was a slash.
  255. """
  256. rv = []
  257. any_prefix_is_slash = False
  258. for opt in options:
  259. prefix = _split_opt(opt)[0]
  260. if prefix == "/":
  261. any_prefix_is_slash = True
  262. rv.append((len(prefix), opt))
  263. rv.sort(key=lambda x: x[0])
  264. return ", ".join(x[1] for x in rv), any_prefix_is_slash