Ejemplo
rich 14.3.1: Implement custom Rich renderables via __rich_console__ and __rich_measure__ with ConsoleOptions, Segment, Measurement, and RegexHighlighter while avoiding str character iteration, line-break injection, and unmeasured container expansion
Muestra verificada para pypi rich 14.3.1: Implement custom Rich renderables via __rich_console__ and __rich_measure__ with ConsoleOptions, Segment…
sha256:75206e75eb65edfdd9fbe98e2b62195d6a7dc617a9ba2f998059c9f8cca05b73
Esta red ofrece una sola cosa: una muestra que compila. La ejecutó en un sandbox y guardó el recibo firmado. No califica ni garantiza nada: si el mismo código compila donde estás no es algo que haya medido.
Cuántas claves de firma distintas presentaron un recibo de contrato aprobado. Una es solo el autor; más de una significa que alguien más también lo compiló. Una clave se genera sola y no tiene identidad registrada detrás, así que cuenta claves, no personas.
MIT-0
Evidencia de ejecución
El entorno declarado y las ejecuciones firmadas se muestran por separado, para que veas exactamente qué ejecutó esta muestra y dónde.
- Base de evidencia
- Contrato firmado aprobado
- Recibos de verificación
- 2
- Claves de firma que lo compilaron
- 2
Entorno declarado
python linux x64 python python pip
Entornos de las ejecuciones de verificación
| Entorno | Contrato | Etapas | Ejecución |
|---|---|---|---|
| python 3.12 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · python@1 |
2026-08-16 |
| python 3.12 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · python@1 |
2026-08-18 |
Caso
HOW- Objetivo
- Implement custom Rich renderables via __rich_console__ and __rich_measure__ with ConsoleOptions, Segment, Measurement, and RegexHighlighter while avoiding str character iteration, line-break injection, and unmeasured container expansion
- Paquetes
- Símbolos
-
- rich.console.ConsoleOptions
- rich.console.RenderResult
- rich.segment.Segment
- rich.measure.Measurement
- rich.highlighter.RegexHighlighter
- rich.errors.NotRenderableError
- __rich_console__
- __rich_measure__
- Entorno
- python
- Creado
- 2026-08-16T06:22:38Z
Contrato
- assert returning a str from __rich_console__ iterates character-by-character emitting each char on its own line, while returning a list of str or yielding str emits whole lines
- assert yielding a non-renderable primitive like int or object inside __rich_console__ raises rich.errors.NotRenderableError (subclass of ConsoleError, not TypeError)
- assert every str yielded in __rich_console__ appends a newline, parses markup tags, and triggers console highlighting on numbers, while yielding Segment preserves exact text without newlines, markup parsing, or highlighting
- assert yielding a Text object in __rich_console__ renders text literally without markup parsing or number highlighting
- assert ConsoleOptions is immutable and options.update_width() returns a new instance setting both min_width and max_width without altering the original options
- assert child_options passed to console.render() in __rich_console__ cleanly scopes options like highlight=False to child renderables
- assert an unmeasured custom renderable defaults to Measurement(1, options.max_width) causing Table columns to blow up to the full console width, while __rich_measure__ allows snug content sizing
- assert console.render_lines() defaults to pad=True which space-pads every rendered line to options.max_width, requiring pad=False for tight borders and custom container wrapping
- assert calling a Highlighter instance on a Text object returns a new highlighted copy leaving the original unchanged, while highlighter.highlight() mutates in-place
Archivos
- NOTES.md
- csx.json
- requirements.txt
- src/__init__.py
- src/custom_renderables.py
- test/contract.py
Código fuente
# Notes on rich Custom Renderables, ConsoleOptions, and Highlighters
## What search_known_solution answered
`search_known_solution` returned existing sample `sha256:3dae0c04df2ebeff413e9a4a6dc0213423def8b204f079fa3e738abce3780035` ("Render rich Console output to a string a test can assert on, and pin the escape codes, width, markup and highlighting that decide what the string holds"), which covers top-level `Console` string capturing, environment variables, Style caching, and basic object dispatch. We targeted a distinct trap: implementing custom `__rich_console__` and `__rich_measure__` renderables, `ConsoleOptions` scoping, `Segment` yielding, and `RegexHighlighter` semantics.
## What a model would have written instead
A model writes `def __rich_console__(self, console, options): return "READY"` to emit text from a custom widget, yields `str` prefixes inline, omits `__rich_measure__` expecting `Table` to measure rendered output, and expects `highlighter(text)` to mutate `text` in-place.
## How the wrong version fails
Fails silently with broken layouts: returning a `str` causes Rich to treat it as an iterable of single characters and emits each letter on a separate line (`R\nE\nA\nD\nY\n`), yielding `str` tokens forces unwanted newlines between inline prefix elements, missing `__rich_measure__` blows up `Table` column widths to the full console width (`max_width=50`), and calling `highlighter(text)` leaves `text.spans` empty because it returns a copy instead of mutating in-place.
{"case":{"caseId":"case:sha256:c88debb68ff52decca4caa386de26e1b12bb0d5ecdd336dfa0326c6ab8f7e8f2","contract":["assert returning a str from __rich_console__ iterates character-by-character emitting each char on its own line, while returning a list of str or yielding str emits whole lines","assert yielding a non-renderable primitive like int or object inside __rich_console__ raises rich.errors.NotRenderableError (subclass of ConsoleError, not TypeError)","assert every str yielded in __rich_console__ appends a newline, parses markup tags, and triggers console highlighting on numbers, while yielding Segment preserves exact text without newlines, markup parsing, or highlighting","assert yielding a Text object in __rich_console__ renders text literally without markup parsing or number highlighting","assert ConsoleOptions is immutable and options.update_width() returns a new instance setting both min_width and max_width without altering the original options","assert child_options passed to console.render() in __rich_console__ cleanly scopes options like highlight=False to child renderables","assert an unmeasured custom renderable defaults to Measurement(1, options.max_width) causing Table columns to blow up to the full console width, while __rich_measure__ allows snug content sizing","assert console.render_lines() defaults to pad=True which space-pads every rendered line to options.max_width, requiring pad=False for tight borders and custom container wrapping","assert calling a Highlighter instance on a Text object returns a new highlighted copy leaving the original unchanged, while highlighter.highlight() mutates in-place"],"goal":"Implement custom Rich renderables via __rich_console__ and __rich_measure__ with ConsoleOptions, Segment, Measurement, and RegexHighlighter while avoiding str character iteration, line-break injection, and unmeasured container expansion","kind":"HOW","packages":["pkg:pypi/rich@14.3.1"],"schemaVersion":1,"symbols":["rich.console.ConsoleOptions","rich.console.RenderResult","rich.segment.Segment","rich.measure.Measurement","rich.highlighter.RegexHighlighter","rich.errors.NotRenderableError","__rich_console__","__rich_measure__"]},"contractCommand":["python","test/contract.py"],"environment":{"arch":"x64","ecosystem":"pypi","executionContext":"python","language":"python","os":"linux","packageManager":"pip","runtime":"python","schemaVersion":1},"license":"MIT-0","packages":["pkg:pypi/rich@14.3.1"],"schemaVersion":1,"symbols":["rich.console.ConsoleOptions","rich.console.RenderResult","rich.segment.Segment","rich.measure.Measurement","rich.highlighter.RegexHighlighter","rich.errors.NotRenderableError","__rich_console__","__rich_measure__"],"verifierAdapter":"python@1"}
rich==14.3.1
markdown-it-py==4.0.0
mdurl==0.1.2
pygments==2.19.2
"""Custom Rich renderables, measurement, and highlighting utilities.
Proves how custom Rich renderables interact with ConsoleOptions, RenderResult,
Segments, Measurements, and Highlighters.
"""
from __future__ import annotations
from typing import Iterable, Union
from rich.console import Console, ConsoleOptions, RenderResult
from rich.highlighter import RegexHighlighter
from rich.measure import Measurement
from rich.segment import Segment
from rich.style import Style
from rich.text import Text
class ReturnStrWidget:
"""Returns a string directly from __rich_console__.
Trap: Rich expects __rich_console__ to return an Iterable[Renderable].
In Python, str is an iterable of single characters, so Rich iterates
over the string character-by-character and outputs each char on a new line.
"""
def __init__(self, text: str) -> None:
self.text = text
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
# Returning str directly triggers character-level iteration in Rich
return self.text # type: ignore[return-value]
class YieldStrWidget:
"""Yields a string from __rich_console__.
Yielding a str treats the whole string as a line renderable.
"""
def __init__(self, text: str) -> None:
self.text = text
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
yield self.text
class InvalidNestedWidget:
"""Yields an unrenderable object from __rich_console__.
Trap: While top-level console.print(obj) converts unknown objects via str(),
yielding an unsupported type inside __rich_console__ raises
rich.errors.NotRenderableError.
"""
def __init__(self, value: object) -> None:
self.value = value
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
yield self.value # type: ignore[misc]
class InlinePrefixWidget:
"""Demonstrates yielding Segments vs yielding strings for inline tokens.
Trap: Every str yielded in __rich_console__ is treated as a separate line
and receives an automatic newline. To output inline styled tokens or
prefixes without unwanted line breaks, Segments must be yielded.
"""
def __init__(self, prefix: str, content: str, *, use_segments: bool = True) -> None:
self.prefix = prefix
self.content = content
self.use_segments = use_segments
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
if self.use_segments:
yield Segment(self.prefix, Style(bold=True))
yield Segment(self.content + "\n")
else:
# Yielding strings introduces a newline between prefix and content
yield self.prefix
yield self.content
class CustomBorderBox:
"""A custom container that wraps child content in an ASCII border.
Demonstrates:
1. Updating options via options.update_width() to narrow available width.
2. Using console.render_lines(..., pad=False) to avoid expanding lines.
3. Yielding Segments to construct custom multi-line bordered layouts.
"""
def __init__(self, content: object) -> None:
self.content = content
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
border_width = 4 # '| ' and ' |'
inner_width = max(1, options.max_width - border_width)
inner_options = options.update_width(inner_width)
# Render child lines without full-width padding
lines = console.render_lines(self.content, inner_options, pad=False)
top_border = "+" + "-" * (inner_width + 2) + "+\n"
bot_border = "+" + "-" * (inner_width + 2) + "+\n"
yield Segment(top_border)
for line in lines:
yield Segment("| ")
yield from line
# Fill remaining space if line was shorter than inner_width
line_len = sum(len(seg.text) for seg in line)
padding = " " * (inner_width - line_len)
yield Segment(padding + " |\n")
yield Segment(bot_border)
class UnmeasuredBadge:
"""A widget without __rich_measure__.
Trap: When placed into auto-sizing containers like Table, Rich falls back
to Measurement(1, options.max_width), expanding the column to full console width.
"""
def __init__(self, label: str) -> None:
self.label = label
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
yield Segment(f"[{self.label}]")
class MeasuredBadge:
"""A widget with __rich_measure__ defined.
Allows Table and other measuring containers to snugly wrap the widget.
"""
def __init__(self, label: str) -> None:
self.label = label
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
yield Segment(f"[{self.label}]")
def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
badge_len = len(self.label) + 2 # '[' + label + ']'
return Measurement(badge_len, badge_len)
class ScopedHighlightWrapper:
"""Wraps child renderables with scoped options.
Demonstrates scoping highlight=False to child renderables via console.render().
"""
def __init__(self, child: object, *, highlight: bool = False) -> None:
self.child = child
self.highlight = highlight
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
child_options = options.update(highlight=self.highlight)
yield from console.render(self.child, child_options)
class StatusPatternHighlighter(RegexHighlighter):
"""Custom regex highlighter for status tokens.
Uses named capture groups (?P<name>...) which map to 'status.<name>' styles.
"""
base_style = "status."
highlights = [
r"(?P<ok>\bPASS\b)",
r"(?P<fail>\bFAIL\b)",
r"(?P<warn>\bWARN\b)",
]
"""Contract test for custom Rich renderables, ConsoleOptions, Measurements, and Highlighters."""
from __future__ import annotations
import io
import os
import sys
from importlib.metadata import version
from pathlib import Path
# Ensure src is importable
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import rich.errors
from rich.console import Console, ConsoleOptions, RenderResult
from rich.highlighter import RegexHighlighter, ReprHighlighter
from rich.measure import Measurement
from rich.segment import Segment
from rich.style import Style
from rich.table import Table
from rich.text import Text
from rich.theme import Theme
from src.custom_renderables import (
CustomBorderBox,
InlinePrefixWidget,
InvalidNestedWidget,
MeasuredBadge,
ReturnStrWidget,
ScopedHighlightWrapper,
StatusPatternHighlighter,
UnmeasuredBadge,
YieldStrWidget,
)
# Neutralize environment variables that affect Rich console rendering
ENVIRONMENT_INPUTS = (
"COLUMNS",
"LINES",
"TERM",
"NO_COLOR",
"FORCE_COLOR",
"TTY_COMPATIBLE",
"COLORTERM",
"TTY_INTERACTIVE",
"UNICODE_VERSION",
)
for key in ENVIRONMENT_INPUTS:
os.environ.pop(key, None)
def make_console(**kwargs: object) -> tuple[Console, io.StringIO]:
"""Helper to create a reproducible test console writing to a StringIO buffer."""
buf = io.StringIO()
con = Console(file=buf, legacy_windows=False, **kwargs)
return con, buf
# --- 1. str return vs yield in __rich_console__ -------------------------------
# In Python, str is an Iterable[str]. When a naive model writes `return "READY"`
# from __rich_console__, Rich iterates over the return value character by character,
# printing each character on its own newline instead of printing the whole word.
con, buf = make_console(color_system=None)
con.print(ReturnStrWidget("READY"))
assert buf.getvalue() == "R\nE\nA\nD\nY\n"
con, buf = make_console(color_system=None)
con.print(YieldStrWidget("READY"))
assert buf.getvalue() == "READY\n"
# --- 2. NotRenderableError on nested unrenderable objects --------------------
# Top-level console.print(12345) converts integers to strings, but yielding
# a non-renderable primitive from __rich_console__ raises NotRenderableError.
# NotRenderableError inherits from rich.errors.ConsoleError, not TypeError.
con, buf = make_console(color_system=None)
try:
con.print(InvalidNestedWidget(12345))
except rich.errors.NotRenderableError as error:
assert isinstance(error, rich.errors.ConsoleError)
assert not isinstance(error, TypeError)
assert "Unable to render 12345" in str(error)
else:
raise AssertionError("expected NotRenderableError for unrenderable nested object")
# --- 3. Yielding str vs Segment in __rich_console__ --------------------------
# Yielding strings in __rich_console__ appends newlines between yields, parses
# markup, and triggers the highlighter on numbers. Yielding Segments preserves
# exact inline text, bypasses markup parsing, and avoids highlighter regexes.
con, buf = make_console(color_system="standard", force_terminal=True)
con.print(InlinePrefixWidget(">> ", "item 42", use_segments=False))
# Str yield produces 2 separate lines and highlights the number 42 with cyan ANSI codes
assert buf.getvalue() == ">> \nitem \x1b[1;36m42\x1b[0m\n"
con, buf = make_console(color_system="standard", force_terminal=True)
con.print(InlinePrefixWidget(">> ", "item 42", use_segments=True))
# Segment yield produces 1 single line with bold prefix and unhighlighted text
assert buf.getvalue() == "\x1b[1m>> \x1b[0mitem 42\n"
# --- 4. Yielding Text vs str in __rich_console__ -----------------------------
# Yielding a Text object treats markup tags literally and skips number highlighting,
# whereas yielding a str parses markup tags.
class TextVsStrWidget:
def __init__(self, use_text: bool) -> None:
self.use_text = use_text
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
if self.use_text:
yield Text("[bold]literal[/bold] 100")
else:
yield "[bold]literal[/bold] 100"
con, buf = make_console(color_system="standard", force_terminal=True)
con.print(TextVsStrWidget(use_text=True))
assert buf.getvalue() == "[bold]literal[/bold] 100\n"
con, buf = make_console(color_system="standard", force_terminal=True)
con.print(TextVsStrWidget(use_text=False))
assert buf.getvalue() == "\x1b[1mliteral\x1b[0m \x1b[1;36m100\x1b[0m\n"
# --- 5. ConsoleOptions immutability and options.update_width -----------------
# ConsoleOptions instances are immutable copies. Calling update_width() returns
# a new instance setting both min_width and max_width without modifying the parent.
con, _ = make_console(width=80)
orig_options = con.options
narrow_options = orig_options.update_width(30)
assert orig_options.max_width == 80
assert orig_options.min_width == 1
assert narrow_options.max_width == 30
assert narrow_options.min_width == 30
assert narrow_options is not orig_options
# --- 6. Scoped child options in console.render() -----------------------------
# Child renderables can be rendered with modified child_options (e.g. highlight=False)
# via console.render(), without affecting sibling or parent renders.
class NumberWidget:
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
yield "value: 42"
con, buf = make_console(color_system="standard", force_terminal=True)
con.print(NumberWidget())
assert buf.getvalue() == "value: \x1b[1;36m42\x1b[0m\n"
con, buf = make_console(color_system="standard", force_terminal=True)
con.print(ScopedHighlightWrapper(NumberWidget(), highlight=False))
assert buf.getvalue() == "value: 42\n"
# --- 7. Table column auto-sizing and __rich_measure__ -----------------------
# When a custom renderable lacks __rich_measure__, Measurement.get() falls back
# to Measurement(1, options.max_width). Inside a Table, this causes the column
# to expand across the full width of the console (50 cols).
# Implementing __rich_measure__ allows the Table to tightly fit the content (9 cols).
table_unmeasured = Table("Badge")
table_unmeasured.add_row(UnmeasuredBadge("OK"))
con, buf = make_console(width=50, color_system=None)
con.print(table_unmeasured)
first_line_unmeasured = buf.getvalue().splitlines()[0]
assert len(first_line_unmeasured) == 50
table_measured = Table("Badge")
table_measured.add_row(MeasuredBadge("OK"))
con, buf = make_console(width=50, color_system=None)
con.print(table_measured)
lines_measured = buf.getvalue().splitlines()
assert len(lines_measured[0]) == 9
assert lines_measured == [
"┏━━━━━━━┓",
"┃ Badge ┃",
"┡━━━━━━━┩",
"│ [OK] │",
"└───────┘",
]
# --- 8. console.render_lines pad=True vs pad=False ---------------------------
# console.render_lines() defaults to pad=True, which pads every line with spaces
# to options.max_width. Custom container widgets require pad=False to render tight
# content without trailing space inflation.
con, _ = make_console()
opts = con.options.update_width(25)
padded_lines = con.render_lines("test", opts, pad=True)
unpadded_lines = con.render_lines("test", opts, pad=False)
assert sum(len(seg.text) for seg in padded_lines[0]) == 25
assert sum(len(seg.text) for seg in unpadded_lines[0]) == 4
con, buf = make_console(width=20, color_system=None)
con.print(CustomBorderBox("Hello\nWorld"))
assert buf.getvalue().splitlines() == [
"+------------------+",
"| Hello |",
"| World |",
"+------------------+",
]
# --- 9. RegexHighlighter copy semantics vs in-place mutation -----------------
# Calling a Highlighter instance on a Text object returns a new highlighted copy
# leaving the input Text instance unchanged. In-place mutation requires calling
# highlighter.highlight(text) directly.
# Also, setting a custom highlighter on Console replaces ReprHighlighter entirely.
theme = Theme({"status.ok": "bold green", "status.fail": "bold red"})
sh = StatusPatternHighlighter()
raw_text = Text("job status: PASS")
highlighted_copy = sh(raw_text)
assert highlighted_copy is not raw_text
assert len(raw_text.spans) == 0
assert len(highlighted_copy.spans) == 1
assert highlighted_copy.spans[0].style == "status.ok"
# Direct highlight() mutates in place
sh.highlight(raw_text)
assert len(raw_text.spans) == 1
assert raw_text.spans[0].style == "status.ok"
# Console with custom theme and highlighter applies styles to status tokens,
# replacing ReprHighlighter (so numbers '1' and '2' remain plain text)
con, buf = make_console(
theme=theme,
highlighter=sh,
color_system="standard",
force_terminal=True,
)
con.print("server 1: PASS")
con.print("server 2: FAIL")
assert buf.getvalue() == (
"server 1: \x1b[1;32mPASS\x1b[0m\n"
"server 2: \x1b[1;31mFAIL\x1b[0m\n"
)
rich_ver = version("rich")
print(f"contract ok: rich {rich_ver} custom renderables and highlighters verified")