Skip to content

[v1.0] Make output renderers honor one validation and width contract #272

Description

@codeforester

Goal

Make plain and Rich tables, plus document-to-record rendering, enforce the same no-loss contract.

Background

render_records() documents deterministic terminal_width and max_cell_width bounds. The plain path calculates and truncates to those widths, but the Rich path receives the original untruncated rows and never receives max_cell_width:

def _write_table(
stream: TextIO,
records: Sequence[Mapping[str, Any]],
columns: Sequence[tuple[str, str]],
footer: str | None,
minimum_widths: Sequence[int] | None,
*,
terminal_width: int | None,
max_cell_width: int | None,
rich: bool,
) -> None:
selected_minimums = minimum_widths or ()
if len(selected_minimums) > len(columns):
raise ValueError("minimum_widths cannot contain more entries than columns")
if not records:
if footer:
stream.write(f"{footer}\n")
return
if not columns:
if footer:
stream.write(f"{footer}\n")
return
table_rows = [[_table_cell(_cell_value(record.get(key))) for _header, key in columns] for record in records]
headers = [_table_cell(header) for header, _key in columns]
widths = [
max(_display_width(header), selected_minimums[index] if index < len(selected_minimums) else 0)
for index, header in enumerate(headers)
]
for row in table_rows:
widths = [max(width, _display_width(value)) for width, value in zip(widths, row, strict=False)]
if max_cell_width is not None:
if max_cell_width < 1:
raise ValueError("max_cell_width must be greater than 0 when set")
widths = [min(width, max_cell_width) for width in widths]
if terminal_width is not None and terminal_width < 1:
raise ValueError("terminal_width must be greater than 0 when set")
available_width = terminal_width if terminal_width is not None else _terminal_width(stream)
widths = _fit_table_width(widths, available_width)
if rich and try_render_rich_table(
stream,
headers,
table_rows,
footer,
terminal_width=terminal_width,
):
return
.

render_document() also silently removes non-mapping elements from a selected list and silently falls back to rendering the entire document when records_key is not a list:

def render_document(
document: Mapping[str, Any],
*,
requested_format: str | None,
records_key: str | None = None,
columns: Sequence[tuple[str, str]] | None = None,
stream: TextIO | None = None,
) -> str:
"""Render a structured report or leave terminal text to its existing renderer.
Structured formats preserve the complete document. Delimited output uses
the selected record list (or the document itself) and never emits report
prose, headers, or footers. A terminal ``text`` request returns ``text``
without writing so the caller can keep its established human report.
"""
target = stream if stream is not None else sys.stdout
resolved = resolve_output_format(requested_format, stream=target)
if resolved == "text":
return resolved
if resolved == "json":
target.write(json.dumps(dict(document), indent=2))
target.write("\n")
return resolved
if resolved == "yaml":
try:
yaml = require_yaml("PyYAML is required for YAML output.")
except RuntimeError as exc:
raise OutputFormatError(str(exc)) from exc
target.write(yaml.safe_dump(dict(document), sort_keys=False, allow_unicode=True))
return resolved
if records_key:
candidate = document.get(records_key)
if isinstance(candidate, list):
records = [record for record in candidate if isinstance(record, Mapping)]
else:
records = [document]
else:
records = [document]
selected_columns = columns or _document_columns(records)
render_records(
. This is a remaining data-loss/fallback variant after #146.

Scope

  • Centralize validated row selection and cell bounding before renderer dispatch.
  • Reject malformed selected record collections with actionable errors.
  • Keep valid JSON/YAML document preservation and delimited streaming behavior.

Acceptance Criteria

  • Rich and plain tables honor identical cell and terminal width limits.
  • Every selected list element is rendered or the call fails; no element is silently dropped.
  • Missing keys, non-list values, and heterogeneous records have explicit documented behavior.
  • Optional Rich failure still falls back deterministically.
  • Tests cover long Unicode/control-bearing cells, empty data, invalid records, TTY/non-TTY, and Rich installed/uninstalled.

Validation

Run output, optional-integration, security, and large-generator tests.

Non-Goals

Do not require Rich or change valid JSON/YAML shapes.

Project Fields

  • Status: Backlog
  • Priority: P1
  • Area: CLI
  • Initiative: v1.0 Readiness
  • Size: M

Ownership

Metadata

Metadata

Assignees

Labels

bugSomething is not working

Type

No type

Projects

Status
Backlog

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions