Coverage for typer / _click / formatting.py: 100%
134 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
1from collections.abc import Generator, Iterable, Iterator, Sequence 1abcdefg
2from contextlib import contextmanager 1abcdefg
4from ._compat import term_len 1abcdefg
5from .parser import _split_opt 1abcdefg
7# Can force a width. This is used by the test system
8FORCED_WIDTH: int | None = None 1abcdefg
11def measure_table(rows: Iterable[tuple[str, str]]) -> tuple[int, ...]: 1abcdefg
12 widths: dict[int, int] = {} 1abcdefg
14 for row in rows: 1abcdefg
15 for idx, col in enumerate(row): 1abcdefg
16 widths[idx] = max(widths.get(idx, 0), term_len(col)) 1abcdefg
18 return tuple(y for x, y in sorted(widths.items())) 1abcdefg
21def iter_rows( 1abcdefg
22 rows: Iterable[tuple[str, str]], col_count: int
23) -> Iterator[tuple[str, ...]]:
24 for row in rows: 1abcdefg
25 yield row + ("",) * (col_count - len(row)) 1abcdefg
28def wrap_text( 1abcdefg
29 text: str,
30 width: int = 78,
31 initial_indent: str = "",
32 subsequent_indent: str = "",
33 preserve_paragraphs: bool = False,
34) -> str:
35 """A helper function that intelligently wraps text. By default, it
36 assumes that it operates on a single paragraph of text but if the
37 `preserve_paragraphs` parameter is provided it will intelligently
38 handle paragraphs (defined by two empty lines).
40 If paragraphs are handled, a paragraph can be prefixed with an empty
41 line containing the ``\\b`` character (``\\x08``) to indicate that
42 no rewrapping should happen in that block.
43 """
44 from ._textwrap import TextWrapper 1abcdefg
46 text = text.expandtabs() 1abcdefg
47 wrapper = TextWrapper( 1abcdefg
48 width,
49 initial_indent=initial_indent,
50 subsequent_indent=subsequent_indent,
51 replace_whitespace=False,
52 )
53 if not preserve_paragraphs: 1abcdefg
54 return wrapper.fill(text) 1abcdefg
56 p: list[tuple[int, bool, str]] = [] 1abcdefg
57 buf: list[str] = [] 1abcdefg
58 indent = None 1abcdefg
60 def _flush_par() -> None: 1abcdefg
61 if not buf: 1abcdefg
62 return 1abcdefg
63 if buf[0].strip() == "\b": 1abcdefg
64 p.append((indent or 0, True, "\n".join(buf[1:]))) 1abcdefg
65 else:
66 p.append((indent or 0, False, " ".join(buf))) 1abcdefg
67 del buf[:] 1abcdefg
69 for line in text.splitlines(): 1abcdefg
70 if not line: 1abcdefg
71 _flush_par() 1abcdefg
72 indent = None 1abcdefg
73 else:
74 if indent is None: 1abcdefg
75 orig_len = term_len(line) 1abcdefg
76 line = line.lstrip() 1abcdefg
77 indent = orig_len - term_len(line) 1abcdefg
78 buf.append(line) 1abcdefg
79 _flush_par() 1abcdefg
81 rv = [] 1abcdefg
82 for indent, raw, text in p: 1abcdefg
83 with wrapper.extra_indent(" " * indent): 1abcdefg
84 if raw: 1abcdefg
85 rv.append(wrapper.indent_only(text)) 1abcdefg
86 else:
87 rv.append(wrapper.fill(text)) 1abcdefg
89 return "\n\n".join(rv) 1abcdefg
92class HelpFormatter: 1abcdefg
93 """This class helps with formatting text-based help pages. It's
94 usually just needed for very special internal cases, but it's also
95 exposed so that developers can write their own fancy outputs.
97 At present, it always writes into memory.
98 """
100 def __init__( 1abcdefg
101 self,
102 indent_increment: int = 2,
103 width: int | None = None,
104 max_width: int | None = None,
105 ) -> None:
106 self.indent_increment = indent_increment 1abcdefg
107 if max_width is None: 1abcdefg
108 max_width = 80 1abcdefg
109 if width is None: 1abcdefg
110 import shutil 1abcdefg
112 width = FORCED_WIDTH 1abcdefg
113 if width is None: 1abcdefg
114 width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50) 1abcdefg
115 self.width = width 1abcdefg
116 self.current_indent: int = 0 1abcdefg
117 self.buffer: list[str] = [] 1abcdefg
119 def write(self, string: str) -> None: 1abcdefg
120 """Writes a unicode string into the internal buffer."""
121 self.buffer.append(string) 1abcdefg
123 def indent(self) -> None: 1abcdefg
124 """Increases the indentation."""
125 self.current_indent += self.indent_increment 1abcdefg
127 def dedent(self) -> None: 1abcdefg
128 """Decreases the indentation."""
129 self.current_indent -= self.indent_increment 1abcdefg
131 def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> None: 1abcdefg
132 """Writes a usage line into the buffer."""
133 if prefix is None: 1abcdefg
134 prefix = "Usage: " 1abcdefg
136 usage_prefix = f"{prefix:>{self.current_indent}}{prog} " 1abcdefg
137 text_width = self.width - self.current_indent 1abcdefg
139 if text_width >= (term_len(usage_prefix) + 20): 1abcdefg
140 # The arguments will fit to the right of the prefix.
141 indent = " " * term_len(usage_prefix) 1abcdefg
142 self.write( 1abcdefg
143 wrap_text(
144 args,
145 text_width,
146 initial_indent=usage_prefix,
147 subsequent_indent=indent,
148 )
149 )
150 else:
151 # The prefix is too long, put the arguments on the next line.
152 self.write(usage_prefix) 1abcdefg
153 self.write("\n") 1abcdefg
154 indent = " " * (max(self.current_indent, term_len(prefix)) + 4) 1abcdefg
155 self.write( 1abcdefg
156 wrap_text(
157 args, text_width, initial_indent=indent, subsequent_indent=indent
158 )
159 )
161 self.write("\n") 1abcdefg
163 def write_heading(self, heading: str) -> None: 1abcdefg
164 """Writes a heading into the buffer."""
165 self.write(f"{'':>{self.current_indent}}{heading}:\n") 1abcdefg
167 def write_paragraph(self) -> None: 1abcdefg
168 """Writes a paragraph into the buffer."""
169 if self.buffer: 1abcdefg
170 self.write("\n") 1abcdefg
172 def write_text(self, text: str) -> None: 1abcdefg
173 """Writes re-indented text into the buffer. This rewraps and
174 preserves paragraphs.
175 """
176 indent = " " * self.current_indent 1abcdefg
177 self.write( 1abcdefg
178 wrap_text(
179 text,
180 self.width,
181 initial_indent=indent,
182 subsequent_indent=indent,
183 preserve_paragraphs=True,
184 )
185 )
186 self.write("\n") 1abcdefg
188 def write_dl( 1abcdefg
189 self,
190 rows: Sequence[tuple[str, str]],
191 col_max: int = 30,
192 col_spacing: int = 2,
193 ) -> None:
194 """Writes a definition list into the buffer. This is how options
195 and commands are usually formatted.
196 """
197 rows = list(rows) 1abcdefg
198 widths = measure_table(rows) 1abcdefg
199 if len(widths) != 2: # pragma: no cover 1abcdefg
200 raise TypeError("Expected two columns for definition list")
202 first_col = min(widths[0], col_max) + col_spacing 1abcdefg
204 for first, second in iter_rows(rows, len(widths)): 1abcdefg
205 self.write(f"{'':>{self.current_indent}}{first}") 1abcdefg
206 if not second: 1abcdefg
207 self.write("\n") 1abcdefg
208 continue 1abcdefg
209 if term_len(first) <= first_col - col_spacing: 1abcdefg
210 self.write(" " * (first_col - term_len(first))) 1abcdefg
211 else:
212 self.write("\n") 1abcdefg
213 self.write(" " * (first_col + self.current_indent)) 1abcdefg
215 text_width = max(self.width - first_col - 2, 10) 1abcdefg
216 wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) 1abcdefg
217 lines = wrapped_text.splitlines() 1abcdefg
219 if lines: 1abcdefg
220 self.write(f"{lines[0]}\n") 1abcdefg
222 for line in lines[1:]: 1abcdefg
223 self.write(f"{'':>{first_col + self.current_indent}}{line}\n") 1abcdefg
224 else: # pragma: no cover
225 self.write("\n")
227 @contextmanager 1abcdefg
228 def section(self, name: str) -> Generator[None]: 1abcdefg
229 """Helpful context manager that writes a paragraph, a heading,
230 and the indents.
231 """
232 self.write_paragraph() 1abcdefg
233 self.write_heading(name) 1abcdefg
234 self.indent() 1abcdefg
235 try: 1abcdefg
236 yield 1abcdefg
237 finally:
238 self.dedent() 1abcdefg
240 @contextmanager 1abcdefg
241 def indentation(self) -> Generator[None]: 1abcdefg
242 """A context manager that increases the indentation."""
243 self.indent() 1abcdefg
244 try: 1abcdefg
245 yield 1abcdefg
246 finally:
247 self.dedent() 1abcdefg
249 def getvalue(self) -> str: 1abcdefg
250 """Returns the buffer contents."""
251 return "".join(self.buffer) 1abcdefg
254def join_options(options: Sequence[str]) -> tuple[str, bool]: 1abcdefg
255 """Given a list of option strings this joins them in the most appropriate
256 way and returns them in the form ``(formatted_string,
257 any_prefix_is_slash)`` where the second item in the tuple is a flag that
258 indicates if any of the option prefixes was a slash.
259 """
260 rv = [] 1abcdefg
261 any_prefix_is_slash = False 1abcdefg
263 for opt in options: 1abcdefg
264 prefix = _split_opt(opt)[0] 1abcdefg
266 if prefix == "/": 1abcdefg
267 any_prefix_is_slash = True 1abcdefg
269 rv.append((len(prefix), opt)) 1abcdefg
271 rv.sort(key=lambda x: x[0]) 1abcdefg
272 return ", ".join(x[1] for x in rv), any_prefix_is_slash 1abcdefg