CodeSampleX

示例

rich 15.0.0: 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

已验证示例 — pypi rich 15.0.0: Render rich Console output to a string a test can assert on, and pin the escape codes, width, markup and highlighting that decide…

sha256:3dae0c04df2ebeff413e9a4a6dc0213423def8b204f079fa3e738abce3780035

本网络只提供一件事:能构建的样本。它在沙箱中运行并保留签名回执。它不评级、不担保——同样的代码能否在你的环境构建,它没有测量过。 提交了通过的契约回执的不同签名密钥数量。为 1 表示只有作者;大于 1 表示还有其他人构建过。密钥是自行生成的,背后没有注册身份,因此计的是密钥而非人。 MIT-0

执行证据

声明的环境与签名的运行分开呈现,你可以看到这个样本究竟运行了什么、在哪里运行。

证据依据
签名契约通过
验证回执
2
构建过它的签名密钥
2
声明的环境 python 3.12 linux x64 python 3.12 python pip

验证运行环境

环境 契约 阶段 运行日期
python 3.12 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1
2026-08-14
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
目标
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
包
符号
  • rich.console.Console
  • Console.capture
  • Console.export_text
  • force_terminal
  • color_system
  • rich.style.Style
  • rich.table.Table
  • rich.markup.escape
  • rich.errors.MarkupError
  • __rich__
  • __rich_console__
  • highlight
环境
python 3.12
创建时间
2026-08-14T13:20:38Z

契约

  1. assert a Console writing to a StringIO reports is_terminal False and color_system None, and emits the markup's text with no escape byte in it
  2. assert force_terminal=True on the same StringIO emits the bold escape sequence, so the codes are a setting rather than a property of the output
  3. assert TERM and COLORTERM decide which color_system is detected and COLORTERM outranks TERM, while neither moves the bytes for bold, which is an attribute and not a colour
  4. assert color_system is settled in the constructor where width is read at access time, so one console follows the environment for its width and ignores it for its colours
  5. assert FORCE_COLOR in the environment turns is_terminal on for a plain file, which is how a CI log fills with escape codes nobody asked for
  6. assert TTY_COMPATIBLE is read before FORCE_COLOR and 0 defeats it while 1 turns is_terminal on by itself, so that fix stops working in a runner exporting it
  7. assert TERM=dumb leaves is_terminal True while dropping color_system to None, so the two are separate switches
  8. assert no_color=True still emits the bold code and only color_system=None suppresses every escape sequence
  9. assert a Style keeps the codes it rendered first, so the console that renders a colour first fixes its bytes for the process and a later console with a different color_system repeats them
  10. assert console.capture() returns the rendered text and redirects rather than tees, so the console's own file receives nothing while the capture is open
  11. assert a capture keeps the escape codes the console would have written, so it is not a way to get plain text
  12. assert nested captures do not nest: the inner result holds everything printed since the outer block began and the outer result holds only what followed
  13. assert record=True with export_text clears the recording by default, so a second call returns an empty string
  14. assert os.get_terminal_size raises with no tty and the console falls back to width 80 by height 25
  15. assert COLUMNS overrides that fallback and is read at access time, so an existing console changes width when the environment does, while an explicit width= beats COLUMNS
  16. assert a small Table renders identically at 40, 80 and 120 columns because it measures its content, while expand=True and overflowing text are what follow the console width
  17. assert markup is interpreted by default and markup=False renders the tags literally, both per-call and per-console
  18. assert markup deletes user text without an error for list[int], for a bracketed sentence and for an unknown style name, on a console emitting codes as well as one that is not
  19. assert a stray closing tag raises rich.errors.MarkupError, which is a ConsoleError and not a ValueError, and that escape() or markup=False prevents it
  20. assert emoji codes are substituted by default and that a Text object is never parsed for markup
  21. assert console.print uses __rich__ first, then __rich_console__, and that __rich__ wins when both exist and is looked up on the instance
  22. assert an object with neither hook is rendered with str() and not repr(), while a container is pretty-printed with repr() on its items
  23. assert the str() branch skips markup and emoji parsing, so printing an object cannot be injected the way printing a string can, while the highlighter is the one rewrite it does not skip
  24. assert a string returned from __rich__ goes through the highlighter, which splits the markup's style run in two on the number inside it
  25. assert the highlighter styles a bare number, a number inside a sentence and the shape of a repr unless highlight=False
  26. assert highlight=False changes nothing on a console that emits no codes, because the highlighter still runs and its spans have nowhere to go
  27. assert rich exports no __version__, so the version has to come from importlib.metadata

文件

  • csx.json
  • requirements.txt
  • src/__init__.py
  • src/report.py
  • test/contract.py

下载源代码构件 (tar.gz)

源代码

csx.json
{"case":{"caseId":"case:sha256:fc38c99aa50800bf226a6ca6ddbb6d1fb7399997594a38f47af776ec9cd64c39","constraints":{"runtime":"python"},"contract":["assert a Console writing to a StringIO reports is_terminal False and color_system None, and emits the markup's text with no escape byte in it","assert force_terminal=True on the same StringIO emits the bold escape sequence, so the codes are a setting rather than a property of the output","assert TERM and COLORTERM decide which color_system is detected and COLORTERM outranks TERM, while neither moves the bytes for bold, which is an attribute and not a colour","assert color_system is settled in the constructor where width is read at access time, so one console follows the environment for its width and ignores it for its colours","assert FORCE_COLOR in the environment turns is_terminal on for a plain file, which is how a CI log fills with escape codes nobody asked for","assert TTY_COMPATIBLE is read before FORCE_COLOR and 0 defeats it while 1 turns is_terminal on by itself, so that fix stops working in a runner exporting it","assert TERM=dumb leaves is_terminal True while dropping color_system to None, so the two are separate switches","assert no_color=True still emits the bold code and only color_system=None suppresses every escape sequence","assert a Style keeps the codes it rendered first, so the console that renders a colour first fixes its bytes for the process and a later console with a different color_system repeats them","assert console.capture() returns the rendered text and redirects rather than tees, so the console's own file receives nothing while the capture is open","assert a capture keeps the escape codes the console would have written, so it is not a way to get plain text","assert nested captures do not nest: the inner result holds everything printed since the outer block began and the outer result holds only what followed","assert record=True with export_text clears the recording by default, so a second call returns an empty string","assert os.get_terminal_size raises with no tty and the console falls back to width 80 by height 25","assert COLUMNS overrides that fallback and is read at access time, so an existing console changes width when the environment does, while an explicit width= beats COLUMNS","assert a small Table renders identically at 40, 80 and 120 columns because it measures its content, while expand=True and overflowing text are what follow the console width","assert markup is interpreted by default and markup=False renders the tags literally, both per-call and per-console","assert markup deletes user text without an error for list[int], for a bracketed sentence and for an unknown style name, on a console emitting codes as well as one that is not","assert a stray closing tag raises rich.errors.MarkupError, which is a ConsoleError and not a ValueError, and that escape() or markup=False prevents it","assert emoji codes are substituted by default and that a Text object is never parsed for markup","assert console.print uses __rich__ first, then __rich_console__, and that __rich__ wins when both exist and is looked up on the instance","assert an object with neither hook is rendered with str() and not repr(), while a container is pretty-printed with repr() on its items","assert the str() branch skips markup and emoji parsing, so printing an object cannot be injected the way printing a string can, while the highlighter is the one rewrite it does not skip","assert a string returned from __rich__ goes through the highlighter, which splits the markup's style run in two on the number inside it","assert the highlighter styles a bare number, a number inside a sentence and the shape of a repr unless highlight=False","assert highlight=False changes nothing on a console that emits no codes, because the highlighter still runs and its spans have nowhere to go","assert rich exports no __version__, so the version has to come from importlib.metadata"],"goal":"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","kind":"HOW","packages":["pkg:pypi/rich@15.0.0"],"schemaVersion":1,"symbols":["rich.console.Console","Console.capture","Console.export_text","force_terminal","color_system","rich.style.Style","rich.table.Table","rich.markup.escape","rich.errors.MarkupError","__rich__","__rich_console__","highlight"]},"contractCommand":["python","test/contract.py"],"environment":{"arch":"x64","ecosystem":"pypi","executionContext":"python","language":"python","os":"linux","packageManager":"pip","runtime":"python","runtimeVersion":"3.12","schemaVersion":1},"license":"MIT-0","packages":["pkg:pypi/rich@15.0.0"],"schemaVersion":1,"symbols":["rich.console.Console","Console.capture","Console.export_text","force_terminal","color_system","rich.style.Style","rich.table.Table","rich.markup.escape","rich.errors.MarkupError","__rich__","__rich_console__","highlight"],"verifierAdapter":"python@1"}
requirements.txt
# The full closure, because it is installed with --no-deps. rich pulls
# markdown-it-py (for rich.markdown) and pygments (for rich.syntax) even
# when you only ever touch Console, and markdown-it-py pulls mdurl.
rich==15.0.0
markdown-it-py==4.2.0
mdurl==0.1.2
pygments==2.20.0
src/__init__.py
src/report.py
"""Rendering rich to a string, because a terminal is not a test fixture.

Console writes to whatever file you hand it, so pointing it at a StringIO
is all it takes to get the output into a variable. What ends up in that
variable is decided by three inputs that are inferred from the environment
unless you set them, which is why the same code prints clean text on a
laptop and escape-code soup in CI:

  is_terminal  - whether rich believes it is driving a terminal. Decided
                 in this order: force_terminal, then TTY_COMPATIBLE, then
                 FORCE_COLOR, and only then file.isatty(). The environment
                 is consulted before the file, which is why a StringIO can
                 report True. TERM is not read here at all.
  color_system - which codes it may emit: "standard", "256", "truecolor",
                 or None for none at all. Detected from COLORTERM first
                 and TERM second, and None when TERM is dumb. It is
                 settled in the constructor, unlike width, so changing the
                 environment afterwards moves an existing Console's width
                 and not its colours.
  width        - asked of the terminal, overridden by the COLUMNS
                 environment variable, and 80 when neither answers.

Those are two independent switches, not one. A dumb terminal is
is_terminal True with color_system None, and no_color=True still emits the
bold code, because bold is not a colour. If you want "no escape codes,
ever", the switch is color_system=None.

string_console pins all three with public Console arguments. Console also
accepts a private _environ mapping, which every one of those lookups goes
through and which would pin the environment instead, but the underscore
is a warning and width= plus force_terminal= cover it.

The output of a Console is not the input you fed it. Markup, emoji codes
and the repr highlighter all rewrite text on the way through, and which of
them run depends on what type you passed. test/contract.py measures the
full dispatch order; the short version is that a str argument is parsed
for markup and a printed object is not.
"""

from __future__ import annotations

import io
import os
from contextlib import contextmanager
from typing import Iterable, Iterator

from rich.console import Console, ConsoleOptions, RenderResult
from rich.table import Table
from rich.text import Text

# Every variable a Console reads when you leave it to guess, outside
# Jupyter - JUPYTER_COLUMNS and JUPYTER_LINES are read only there. A test
# that does not neutralise these is testing the machine it runs on, and
# the four that a list written from memory leaves out are the ones that
# bite: TTY_COMPATIBLE is checked before FORCE_COLOR and "0" beats it,
# COLORTERM outranks TERM when the colour system is detected,
# TTY_INTERACTIVE sets is_interactive, and UNICODE_VERSION chooses the
# table rich measures character widths against.
ENVIRONMENT_INPUTS = ("COLUMNS", "LINES", "TERM", "NO_COLOR", "FORCE_COLOR",
                      "TTY_COMPATIBLE", "COLORTERM", "TTY_INTERACTIVE",
                      "UNICODE_VERSION")


@contextmanager
def pinned_environment(**overrides: str) -> Iterator[None]:
    """Run a block with rich's environment inputs cleared, then restored.

    Values passed as keyword arguments are set instead of cleared, which is
    how the contract reproduces a CI runner that exports FORCE_COLOR.
    """
    saved = {key: os.environ.get(key) for key in ENVIRONMENT_INPUTS}
    saved.update({key: os.environ.get(key) for key in overrides})
    try:
        for key in ENVIRONMENT_INPUTS:
            os.environ.pop(key, None)
        os.environ.update(overrides)
        yield
    finally:
        for key, value in saved.items():
            if value is None:
                os.environ.pop(key, None)
            else:
                os.environ[key] = value


def string_console(
    *, width: int = 80, color: bool = False, **overrides: object
) -> tuple[Console, io.StringIO]:
    """A Console writing to a string, with nothing left to the environment.

    color=False is not the same thing as force_terminal=False alone:
    force_terminal decides whether rich thinks it is on a terminal, and
    color_system decides whether it may emit codes. Setting both is what
    makes the output identical on a developer's tty and in a CI pipe.

    width= is what pins a Table. It beats COLUMNS, which is the variable
    that quietly re-wraps golden output when a CI image sets it.
    """
    buffer = io.StringIO()
    console = Console(
        file=buffer,
        width=width,
        force_terminal=color,
        color_system="standard" if color else None,
        # Left alone this is detected rather than off: it turns on for a
        # Windows console without VT support, and legacy mode substitutes
        # the box-drawing characters and clamps colours to the sixteen the
        # old console had. A test asserting on box characters has to say
        # which set it means.
        legacy_windows=False,
        **overrides,
    )
    return console, buffer


def render_to_string(renderable: object, *, width: int = 80,
                     color: bool = False) -> str:
    """Render one thing and return the text, with no console left over."""
    console, buffer = string_console(width=width, color=color)
    console.print(renderable)
    return buffer.getvalue()


def capture_to_string(renderable: object, *, width: int = 80,
                      color: bool = False) -> str:
    """The same thing through console.capture().

    capture() redirects rather than tees: the console's file receives
    nothing while a capture is open. It exists for the case where you do
    not own the Console - a library that prints to its own global console
    can be captured this way, where swapping in a StringIO cannot.
    """
    console, _ = string_console(width=width, color=color)
    with console.capture() as capture:
        console.print(renderable)
    return capture.get()


def status_table(rows: Iterable[tuple[str, int]]) -> Table:
    """A small table, deliberately narrower than any console width.

    A Table sizes itself to its content and stops. It only follows the
    console width when it is told to (expand=True) or when the content
    does not fit, which is the difference between "the width is stable"
    and "the width does not depend on the environment".
    """
    table = Table("name", "count")
    for name, count in rows:
        table.add_row(name, str(count))
    return table


class Duration:
    """Renders through __rich__, which returns a string with markup in it.

    __rich__ is consulted before anything else, and a string it returns is
    parsed for markup just like a string you passed to print yourself. An
    object is not automatically the safe way to print untrusted data; a
    __rich__ that interpolates user text has the same hole as print().
    """

    def __init__(self, seconds: float) -> None:
        self.seconds = seconds

    def __repr__(self) -> str:
        return f"Duration({self.seconds!r})"

    def __rich__(self) -> str:
        style = "red" if self.seconds > 1 else "green"
        return f"[{style}]{self.seconds:.2f}s[/{style}]"


class Checklist:
    """Renders through __rich_console__, which yields more than one line.

    __rich_console__ is the interface for something that has to look at
    the console or its options - the width it is being rendered into, for
    instance - and can emit several renderables. It is only reached if the
    object has no __rich__.
    """

    def __init__(self, items: Iterable[str]) -> None:
        self.items = list(items)

    def __repr__(self) -> str:
        return f"Checklist({self.items!r})"

    def __rich_console__(self, console: Console,
                         options: ConsoleOptions) -> RenderResult:
        for item in self.items:
            yield Text(f"[x] {item}")
        yield Text(f"{len(self.items)} done, width {options.max_width}")


class Job:
    """No rich hooks at all, and __str__ differs from __repr__ on purpose.

    Which one rich uses is the assertion in test/contract.py, and the
    answer is not the one the documentation's phrasing suggests.
    """

    def __init__(self, name: str) -> None:
        self.name = name

    def __str__(self) -> str:
        return f"job {self.name}"

    def __repr__(self) -> str:
        return f"Job(name={self.name!r})"


class Both(Duration):
    """Defines both hooks, to settle which one wins."""

    def __rich_console__(self, console: Console,
                         options: ConsoleOptions) -> RenderResult:
        yield Text("from __rich_console__")
test/contract.py
import io
import os
import sys
from importlib.metadata import version
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

import rich
import rich.errors
from rich.console import Console
from rich.highlighter import ReprHighlighter
from rich.markup import escape
from rich.style import Style
from rich.table import Table
from rich.text import Text

from src.report import (ENVIRONMENT_INPUTS, Both, Checklist, Duration, Job,
                        capture_to_string, pinned_environment,
                        render_to_string, status_table, string_console)

# Every assertion below is about what rich reads from its environment, so
# the environment is cleared first. A suite that skips this step passes on
# a laptop and fails in a runner that exports COLUMNS or FORCE_COLOR.
for key in ENVIRONMENT_INPUTS:
    os.environ.pop(key, None)

# The width fallback asserted further down is only the fallback when no
# standard stream is a terminal. That is true here - the sandbox has no
# tty - and this check turns a surprising width into a clear message if
# somebody re-runs the contract with docker -t.
assert not any(stream is not None and stream.isatty() for stream in
               (sys.__stdin__, sys.__stdout__, sys.__stderr__))


def plain(**kwargs):
    buffer = io.StringIO()
    return Console(file=buffer, **kwargs), buffer


# --- escape codes are a setting, not a property of the output -----------
# A StringIO is not a terminal, so rich emits nothing but text: markup is
# applied to the text and then thrown away for want of anywhere to put it.
console, buffer = plain()
assert console.is_terminal is False
assert console.color_system is None
console.print("[bold]hello[/bold]")
assert buffer.getvalue() == "hello\n"
assert "\x1b" not in buffer.getvalue()

console, buffer = plain(force_terminal=False)
console.print("[bold]hello[/bold]")
assert buffer.getvalue() == "hello\n"

# The same call with the switch flipped. Same text, different bytes. The
# detected color_system is "standard" only because TERM was cleared above.
console, buffer = plain(force_terminal=True)
assert console.is_terminal is True
assert console.color_system == "standard"
console.print("[bold]hello[/bold]")
assert buffer.getvalue() == "\x1b[1mhello\x1b[0m\n"

# What TERM and COLORTERM decide is which color_system is detected, and
# COLORTERM outranks TERM rather than the other way round. MEASURED, and
# it narrows the usual advice: neither one moves the bytes for bold, which
# is an attribute and not a colour. What makes the assertion above stable
# is clearing the environment, not pinning color_system.
with pinned_environment(TERM="xterm-256color"):
    console, buffer = plain(force_terminal=True)
    assert console.color_system == "256"
    console.print("[bold]hello[/bold]")
    assert buffer.getvalue() == "\x1b[1mhello\x1b[0m\n"
with pinned_environment(TERM="xterm-256color", COLORTERM="truecolor"):
    assert plain(force_terminal=True)[0].color_system == "truecolor"

# color_system is settled in the constructor, while width is read at
# access time (asserted further down). One console therefore follows the
# environment for its width and ignores it for its colours.
console, _ = plain(force_terminal=True)
with pinned_environment(TERM="xterm-256color"):
    assert console.color_system == "standard"
    assert plain(force_terminal=True)[0].color_system == "256"

# Nobody has to set force_terminal for that to happen in CI. FORCE_COLOR
# in the environment turns is_terminal on for a plain file object, which
# is how a log full of escape codes gets produced by code that never asked
# for colour. It is a configuration, and the fix is the same switch.
with pinned_environment(FORCE_COLOR="1"):
    console, buffer = plain()
    assert console.is_terminal is True
    console.print("[bold]hello[/bold]")
    assert buffer.getvalue() == "\x1b[1mhello\x1b[0m\n"

# TTY_COMPATIBLE is read before FORCE_COLOR and "0" beats it, so the fix
# for that log stops working in a runner that exports it, with nothing
# said. It also decides is_terminal on its own. This suite clears it for
# the same reason it clears the rest: set to "0" in the environment, it
# would fail the assertion directly above.
with pinned_environment(TTY_COMPATIBLE="0", FORCE_COLOR="1"):
    console, buffer = plain()
    assert console.is_terminal is False
    assert console.color_system is None
    console.print("[bold]hello[/bold]")
    assert buffer.getvalue() == "hello\n"
with pinned_environment(TTY_COMPATIBLE="1"):
    console = plain()[0]
    assert console.is_terminal is True
    assert console.color_system == "standard"

# is_terminal and color_system are two switches, not one. TERM=dumb leaves
# is_terminal True and drops color_system to None, so a "terminal" console
# emits no codes at all.
with pinned_environment(TERM="dumb", FORCE_COLOR="1"):
    console, buffer = plain()
    assert console.is_terminal is True
    assert console.color_system is None
    console.print("[bold]hello[/bold]")
    assert buffer.getvalue() == "hello\n"

# no_color is the switch people reach for, and it is not the one they
# want: it suppresses colour and leaves every other attribute alone, so
# bold still arrives as an escape sequence.
console, buffer = plain(force_terminal=True, no_color=True)
console.print("[bold red]hello[/bold red]")
assert buffer.getvalue() == "\x1b[1mhello\x1b[0m\n"

# color_system=None is the one that means "no escape codes, ever", and it
# holds even with force_terminal=True.
console, buffer = plain(force_terminal=True, color_system=None)
console.print("[bold red]hello[/bold red]")
assert buffer.getvalue() == "hello\n"

# --- a style keeps the codes it rendered the first time -----------------
# MEASURED, and for anyone asserting on exact bytes it outranks every
# switch above: Style stores the codes it rendered in Style._ansi and
# reuses them, and Style.parse returns the same object for the same
# markup, so the first console to render a style fixes its bytes for the
# rest of the process. A truecolor console writes #00aaff as 24-bit, and a
# standard console asked for that colour afterwards repeats those bytes
# instead of downgrading to the nearest of sixteen.
console, buffer = plain(force_terminal=True, color_system="truecolor")
console.print("[#00aaff]x[/#00aaff]")
assert buffer.getvalue() == "\x1b[38;2;0;170;255mx\x1b[0m\n"
console, buffer = plain(force_terminal=True, color_system="standard")
console.print("[#00aaff]x[/#00aaff]")
assert buffer.getvalue() == "\x1b[38;2;0;170;255mx\x1b[0m\n"

# The same thing the other way round, with a colour nothing has rendered
# yet: standard first flattens it to green, and the truecolor console that
# follows is stuck with green.
console, buffer = plain(force_terminal=True, color_system="standard")
console.print("[#00bb44]x[/#00bb44]")
assert buffer.getvalue() == "\x1b[32mx\x1b[0m\n"
console, buffer = plain(force_terminal=True, color_system="truecolor")
console.print("[#00bb44]x[/#00bb44]")
assert buffer.getvalue() == "\x1b[32mx\x1b[0m\n"

# The cache lives on the Style, which is where to look when bytes that
# should differ do not.
style = Style.parse("#00bb44")
assert style._ansi == "32"
assert Style.parse("#00bb44") is style

# --- capture() returns the rendered text --------------------------------
console, buffer = string_console()
with console.capture() as capture:
    console.print("captured")
console.print("after")

# It redirects, it does not tee: nothing written inside the block reached
# the console's own file. Asserting on both is the only way to see that.
assert capture.get() == "captured\n"
assert buffer.getvalue() == "after\n"

# capture() renders, so it captures whatever the console would have
# written - escape codes included. It is not a way to get plain text.
assert capture_to_string("[bold]x[/bold]") == "x\n"
assert capture_to_string("[bold]x[/bold]", color=True) == "\x1b[1mx\x1b[0m\n"

# Nested captures do not nest. The inner block ends the capture that the
# outer block started, so the inner result holds everything printed since
# the outer `with`, and the outer result holds only what came after it.
console, buffer = string_console()
with console.capture() as outer:
    console.print("a")
    with console.capture() as inner:
        console.print("b")
    console.print("c")
assert inner.get() == "a\nb\n"
assert outer.get() == "c\n"

# record=True is the other way out, and export_text clears the recording
# by default, so the second call on the same console returns nothing.
console = Console(file=io.StringIO(), record=True, force_terminal=True,
                  color_system="standard", legacy_windows=False)
console.print("[bold]rec[/bold]")
assert console.export_text() == "rec\n"
assert console.export_text() == ""
console.print("[bold]rec[/bold]")
assert console.export_text(styles=True, clear=False) == "\x1b[1mrec\x1b[0m\n"
assert console.export_text(styles=True, clear=False) == "\x1b[1mrec\x1b[0m\n"

# --- width: 80 is a fallback, and COLUMNS overrides it ------------------
# No stream is a terminal here, so os.get_terminal_size has nothing to
# answer with and rich falls back to 80 by 25.
try:
    os.get_terminal_size()
except OSError:
    pass
else:
    raise AssertionError("expected no terminal size in the sandbox")

console, _ = plain()
assert console.width == 80
assert console.size == (80, 25)

# COLUMNS wins over the detected size, and width is read at access time,
# not at construction: the same console object answers differently after
# the environment changes underneath it.
with pinned_environment(COLUMNS="120"):
    assert console.width == 120
    assert plain()[0].width == 120
assert console.width == 80

# An explicit width beats COLUMNS, which is why string_console sets one.
with pinned_environment(COLUMNS="120"):
    assert plain(width=60)[0].width == 60

# MEASURED, against the usual advice: a Table does not fill the console.
# It measures its content and stops, so this one is 17 columns wide inside
# a 40, an 80 and a 120 column console. Pinning the console width does not
# make a small table stable - it already was, and the rows are the proof.
table_rows = [("alpha", 1), ("beta", 22)]
narrow = render_to_string(status_table(table_rows), width=40)
assert narrow.splitlines() == [
    "┏━━━━━━━┳━━━━━━━┓",
    "┃ name  ┃ count ┃",
    "┡━━━━━━━╇━━━━━━━┩",
    "│ alpha │ 1     │",
    "│ beta  │ 22    │",
    "└───────┴───────┘",
]
assert render_to_string(status_table(table_rows), width=80) == narrow
assert render_to_string(status_table(table_rows), width=120) == narrow
assert {len(line) for line in narrow.splitlines()} == {17}

# What does follow the console width is expand=True, and content too wide
# to fit. Those are the renders that move when COLUMNS moves, and the only
# ones an explicit width= actually pins.
expanding = Table("name", "count", expand=True)
expanding.add_row("alpha", "1")
for width in (40, 80, 120):
    rendered = render_to_string(expanding, width=width)
    assert {len(line) for line in rendered.splitlines()} == {width}

assert [len(line) for line in
        render_to_string("z" * 100, width=80).splitlines()] == [80, 20]
assert [len(line) for line in
        render_to_string("z" * 100, width=120).splitlines()] == [100]

# --- markup is on by default, and it rewrites user data -----------------
assert render_to_string("[bold]x[/bold]") == "x\n"
assert render_to_string("[bold]x[/bold]", color=True) == "\x1b[1mx\x1b[0m\n"

console, buffer = string_console()
console.print("[bold]x[/bold]", markup=False)
assert buffer.getvalue() == "[bold]x[/bold]\n"

console, buffer = string_console(markup=False)
console.print("[bold]x[/bold]")
assert buffer.getvalue() == "[bold]x[/bold]\n"

# Two shapes of silent deletion, for text that came from somewhere else:
# a generic type name loses its parameter, and a sentence in square
# brackets renders as an empty line. Neither raises anything.
assert render_to_string("list[int]") == "list\n"
assert render_to_string("[not a tag]") == "\n"

# An unknown style is swallowed without an error, taking the tags with it,
# and it is swallowed on a console that emits codes too - the name does
# not have to resolve for the text to survive and the tags to vanish.
assert render_to_string("[nosuchstyle]hi[/nosuchstyle]") == "hi\n"
console, buffer = string_console(color=True)
console.print("[nosuchstyle]hi[/nosuchstyle]")
assert buffer.getvalue() == "hi\n"

# And a stray closing bracket raises, so printing user text can end the
# process. MarkupError descends from ConsoleError, not from ValueError, so
# the except clause a defensive caller already wrote does not catch it.
try:
    render_to_string("user said [/wat]")
except rich.errors.MarkupError as error:
    assert "doesn't match any open tag" in str(error)
    assert not isinstance(error, ValueError)
    assert isinstance(error, rich.errors.ConsoleError)
else:
    raise AssertionError("stray closing tag did not raise")

# The two fixes: escape() at the point of interpolation, or markup=False.
assert render_to_string(escape("user said [/wat]")) == "user said [/wat]\n"
console, buffer = string_console(markup=False)
console.print("user said [/wat]")
assert buffer.getvalue() == "user said [/wat]\n"

# Markup is not the only rewrite. Emoji codes are substituted by default,
# which is the same class of surprise for logged text.
assert render_to_string("build :rocket: done") == "build \U0001f680 done\n"
console, buffer = string_console(emoji=False)
console.print("build :rocket: done")
assert buffer.getvalue() == "build :rocket: done\n"

# A Text object is never parsed for markup, which is what makes it the
# right container for text you did not write.
console, buffer = string_console()
console.print(Text("[bold]x[/bold]"))
assert buffer.getvalue() == "[bold]x[/bold]\n"

# --- what console.print does with an object -----------------------------
# __rich__ first. It returns a string here, and that string IS parsed for
# markup, so an object is not a way to escape the hazard above.
assert render_to_string(Duration(0.5)) == "0.50s\n"

# MEASURED: a string from __rich__ takes the whole string pipeline, which
# includes the highlighter, and the highlighter cuts the style run in two.
# "0." matches its number pattern and gets bold on top of the green, so
# "0.50s" arrives as two escape runs instead of the one the markup asked
# for. Any golden test on a __rich__ object has to expect this.
assert (render_to_string(Duration(0.5), color=True)
        == "\x1b[1;32m0.\x1b[0m\x1b[32m50s\x1b[0m\n")
assert (render_to_string(Duration(2.0), color=True)
        == "\x1b[1;31m2.\x1b[0m\x1b[31m00s\x1b[0m\n")

# highlight=False leaves the markup's own style alone, one run as written.
console, buffer = string_console(color=True)
console.print(Duration(0.5), highlight=False)
assert buffer.getvalue() == "\x1b[32m0.50s\x1b[0m\n"

# __rich_console__ next, for objects that need the console or its options
# and emit more than one renderable. max_width is the width the object is
# being rendered into, so it tracks the console.
assert render_to_string(Checklist(["a", "b"]), width=40) == (
    "[x] a\n[x] b\n2 done, width 40\n")
assert render_to_string(Checklist(["a", "b"]), width=80).endswith(
    "width 80\n")

# Both defined: __rich__ wins, because rich_cast runs before anything
# looks for a renderable, and it returns a string that is not one.
assert render_to_string(Both(0.5)) == "0.50s\n"

# __rich__ is looked up on the instance, not only on the type - unlike
# every dunder Python itself dispatches. Patching one object works.
job = Job("alpha")
job.__rich__ = lambda: "patched"
assert render_to_string(job) == "patched\n"

# MEASURED, and it contradicts the usual summary of this API: an object
# with no rich hooks is rendered with str(), not repr(). The two are the
# same for most objects, which is why "it uses repr" survives as folklore
# - here they differ, and str wins.
assert str(Job("alpha")) != repr(Job("alpha"))
assert render_to_string(Job("alpha")) == "job alpha\n"

# repr does appear, one level down: a container is pretty-printed, and the
# pretty printer uses repr on its items.
assert render_to_string([Job("alpha")]) == "[Job(name='alpha')]\n"

# The str() branch skips markup and emoji parsing entirely, so an object
# is safe from the injection above in a way a bare string is not.
assert render_to_string(Job("[bold]x[/bold]")) == "job [bold]x[/bold]\n"
assert render_to_string(Job(":rocket:")) == "job :rocket:\n"

# The highlighter is the one rewrite it does not skip: the number in the
# same string is styled even though the markup in it was left alone.
console, buffer = string_console(color=True)
console.print(Job("42 items"))
assert buffer.getvalue() == "job \x1b[1;36m42\x1b[0m items\n"
console, buffer = string_console(color=True)
console.print(Job("42 items"), highlight=False)
assert buffer.getvalue() == "job 42 items\n"

# --- the highlighter rewrites numbers, unless you turn it off -----------
assert render_to_string(123, color=True) == "\x1b[1;36m123\x1b[0m\n"

console, buffer = string_console(color=True)
console.print(123, highlight=False)
assert buffer.getvalue() == "123\n"

console, buffer = string_console(color=True, highlight=False)
console.print(123)
assert buffer.getvalue() == "123\n"

# It is a regex pass over the text, not a rule about the argument, so a
# number inside a sentence is styled too, and so is the shape of a repr.
console, buffer = string_console(color=True)
console.print("the value is 42 ok")
assert buffer.getvalue() == "the value is \x1b[1;36m42\x1b[0m ok\n"

console, buffer = string_console(color=True)
console.print([1])
assert buffer.getvalue() == "\x1b[1m[\x1b[0m\x1b[1;36m1\x1b[0m\x1b[1m]\x1b[0m\n"

# MEASURED, and it narrows the claim: on a console that emits no codes,
# highlight=False changes nothing at all, because the highlighter still
# runs and its spans have nowhere to go. Turning it off is only visible
# where colour is.
assert render_to_string(123) == "123\n"
console, buffer = string_console(highlight=False)
console.print(123)
assert buffer.getvalue() == "123\n"
assert ReprHighlighter()("123").spans[0].style == "repr.number"

# The version has to come from importlib.metadata: rich exports no
# __version__, so a test helper that prints one raises AttributeError.
assert not hasattr(rich, "__version__")
print("contract ok: rich", version("rich"), "console width fallback",
      Console(file=io.StringIO()).width)

原始种子者

anonymous