Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 86 additions & 87 deletions src/zenml/cli/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# permissions and limitations under the License.
"""Helper functions to format output for CLI."""

from typing import Dict, Iterable, Iterator, Optional, Sequence, Tuple
from typing import Dict, Iterable, Iterator, List, Optional, Sequence, Tuple

from click import formatting
from click._compat import term_len
Expand Down Expand Up @@ -79,105 +79,104 @@ def write_dl(
col_max: int = 30,
col_spacing: int = 2,
) -> None:
"""Writes a definition list into the buffer.
"""Writes a definition list into the formatter buffer.

This is how options and commands are usually formatted.
Click 8.2 tightened validation so that definition list entries must be
pairs. Our CLI groups commands in tagged triples, so we detect that
case and render it manually while delegating the standard behavior to
Click for the classic two-column output. This keeps the formatter
compatible with both older Click releases (<8.2) and the newer ones
without sacrificing the custom grouped layout.

Arguments:
rows: a list of items as tuples for the terms and values.
col_max: the maximum width of the first column.
col_spacing: the number of spaces between the first and
second column (and third).

The default behavior is to format the rows in a definition list
with rows of 2 columns following the format ``(term, value)``.
But for new CLI commands, we want to format the rows in a definition
list with rows of 3 columns following the format
``(term, value, description)``.
Args:
rows: A sequence of tuples that represent the definition list
entries.
col_max: The maximum width of the first column.
col_spacing: The number of spaces between columns.

Raises:
TypeError: if the number of columns is not 2 or 3.
TypeError: If the provided rows do not represent two or three
column entries.
"""
rows = list(rows)
normalized_rows: List[Tuple[str, ...]] = [tuple(row) for row in rows]

if not normalized_rows:
return

unique_lengths = {len(row) for row in normalized_rows}

if unique_lengths == {2}:
two_col_rows = [(row[0], row[1]) for row in normalized_rows]
super().write_dl(
two_col_rows, col_max=col_max, col_spacing=col_spacing
)
return

if unique_lengths == {3}:
self._write_triple_definition_list(
[(row[0], row[1], row[2]) for row in normalized_rows],
col_max=col_max,
col_spacing=col_spacing,
)
return

raise TypeError(
"Expected either two- or three-column definition list entries."
)

def _write_triple_definition_list(
self,
rows: List[Tuple[str, str, str]],
col_max: int,
col_spacing: int,
) -> None:
widths = measure_table(rows)

if len(widths) == 2:
first_col = min(widths[0], col_max) + col_spacing

for first, second in iter_rows(rows, len(widths)):
self.write(f"{'':>{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 = formatting.wrap_text(
second, text_width, preserve_paragraphs=True
if len(widths) < 3:
raise TypeError(
"Expected three columns for tagged definition list entries."
)

first_col = min(widths[0], col_max) + col_spacing
second_col = min(widths[1], col_max) + col_spacing * 2

current_tag: Optional[str] = None

for first, second, third in iter_rows(rows, 3):
if current_tag != first:
current_tag = first
self.write("\n")
self.write(
f"[#431d93]{'':>{self.current_indent}}{first}:[/#431d93]\n"
)
lines = wrapped_text.splitlines()

if lines:
self.write(f"{lines[0]}\n")

for line in lines[1:]:
self.write(
f"{'':>{first_col + self.current_indent}}{line}\n"
)
else:
self.write("\n")

elif len(widths) == 3:
first_col = min(widths[0], col_max) + col_spacing
second_col = min(widths[1], col_max) + col_spacing * 2

current_tag = None
for first, second, third in iter_rows(rows, len(widths)):
if current_tag != first:
current_tag = first
self.write("\n")
# Adding [#431d93] [/#431d93] makes the tag colorful when
# it is printed by rich print
self.write(
f"[#431d93]{'':>{self.current_indent}}{first}:[/#431d93]\n"
)

if not third:
self.write("\n")
continue
if not third:
self.write("\n")
continue

if term_len(first) <= first_col - col_spacing:
self.write(" " * self.current_indent * 2)
else:
self.write("\n")
self.write(" " * (first_col + self.current_indent))

if term_len(first) <= first_col - col_spacing:
self.write(" " * self.current_indent * 2)
else:
self.write("\n")
self.write(" " * (first_col + self.current_indent))
self.write(f"{'':>{self.current_indent}}{second}")

self.write(f"{'':>{self.current_indent}}{second}")
text_width = max(self.width - second_col - 4, 10)
wrapped_text = formatting.wrap_text(
third, text_width, preserve_paragraphs=True
)
lines = wrapped_text.splitlines()

text_width = max(self.width - second_col - 4, 10)
wrapped_text = formatting.wrap_text(
third, text_width, preserve_paragraphs=True
if lines:
self.write(
" " * (second_col - term_len(second) + self.current_indent)
)
lines = wrapped_text.splitlines()
self.write(f"{lines[0]}\n")

if lines:
for line in lines[1:]:
self.write(
" "
* (second_col - term_len(second) + self.current_indent)
f"{'':>{second_col + self.current_indent * 4}}{line}\n"
)
self.write(f"{lines[0]}\n")

for line in lines[1:]:
self.write(
f"{'':>{second_col + self.current_indent * 4}}{line}\n"
)
else:
self.write("\n")
else:
raise TypeError(
"Expected either three or two columns for definition list"
)
else:
self.write("\n")
Loading