CodeSampleX

샘플

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

검증된 샘플 — pypi rich 14.3.1: Implement custom Rich renderables via __rich_console__ and __rich_measure__ with ConsoleOptions, Segment, Measurement, and…

sha256:75206e75eb65edfdd9fbe98e2b62195d6a7dc617a9ba2f998059c9f8cca05b73

이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다. 통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다. MIT-0

실행 증거

선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.

증거 기준
서명된 컨트랙트 통과
검증 영수증
2
빌드한 서명 키
2
선언된 환경 python linux x64 python python pip

검증 실행 환경

환경 컨트랙트 단계 실행일
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

케이스

HOW
목표
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
패키지
심벌
  • rich.console.ConsoleOptions
  • rich.console.RenderResult
  • rich.segment.Segment
  • rich.measure.Measurement
  • rich.highlighter.RegexHighlighter
  • rich.errors.NotRenderableError
  • __rich_console__
  • __rich_measure__
환경
python
생성일
2026-08-16T06:22:38Z

컨트랙트

  1. 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
  2. assert yielding a non-renderable primitive like int or object inside __rich_console__ raises rich.errors.NotRenderableError (subclass of ConsoleError, not TypeError)
  3. 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
  4. assert yielding a Text object in __rich_console__ renders text literally without markup parsing or number highlighting
  5. assert ConsoleOptions is immutable and options.update_width() returns a new instance setting both min_width and max_width without altering the original options
  6. assert child_options passed to console.render() in __rich_console__ cleanly scopes options like highlight=False to child renderables
  7. 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
  8. 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
  9. assert calling a Highlighter instance on a Text object returns a new highlighted copy leaving the original unchanged, while highlighter.highlight() mutates in-place

파일

  • NOTES.md
  • csx.json
  • requirements.txt
  • src/__init__.py
  • src/custom_renderables.py
  • test/contract.py

소스 아티팩트 내려받기 (tar.gz)

소스

NOTES.md
# 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.
csx.json
{"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"}
requirements.txt
rich==14.3.1
markdown-it-py==4.0.0
mdurl==0.1.2
pygments==2.19.2
src/__init__.py
src/custom_renderables.py
"""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)",
    ]
test/contract.py
"""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")

오리진 시더

csx-seed