Rich Console Protocol 完全指南:为自定义对象打造终端富文本渲染能力
【免费下载链接】richRich is a Python library for rich text and beautiful formatting in the terminal.项目地址: https://gitcode.com/gh_mirrors/ri/rich
Rich(rich)是一款用于在终端输出富文本与精美格式化的 Python 库。本文聚焦 Rich 最核心的扩展机制——Console Protocol(控制台协议),它允许你为自定义对象接入 Rich 的渲染管线,从而让Console.print()、日志输出具备颜色、样式与表格等富文本能力。读完本文,你将掌握__rich__、__rich_console__、__rich_measure__三种协议方法的签名与用法,能够把任意自定义类渲染成带样式的高质量终端输出,并理解其背后的源码实现原理。
本文对应的官方协议文档位于 docs/source/protocol.rst,源码实现位于 rich/protocol.py、rich/console.py 与 rich/measure.py。
一、为什么需要 Console Protocol
Rich 的Console对象可以打印str、Text、Table、Panel等内建渲染对象,但默认情况下,如果你直接console.print()一个自定义类的实例,只会得到类似<__main__.MyObject object at 0x...>的__repr__字符串,信息量有限且难以阅读。
Console Protocol 就是为了解决这个问题而设计的一套轻量接口:只要你的类实现了协议规定的方法,Rich 就能以完全定制的方式渲染它。官方文档指出,这套协议适用于两类场景:
- 展示(presentation):让对象在终端中以美观、结构化的方式呈现;
- 调试信息增强:展示比典型
__repr__字符串更难解析的调试细节。
从源码看,Rich 判定一个对象是否可渲染,依据正是协议方法的存在性。rich/protocol.py 中的is_renderable()定义如下:
def is_renderable(check_object: Any) -> bool: """Check if an object may be rendered by Rich.""" return ( isinstance(check_object, str) or hasattr(check_object, "__rich__") or hasattr(check_object, "__rich_console__") )也就是说:普通字符串天然可渲染;实现了__rich__或__rich_console__的对象可渲染。这正是整个协议体系的判断基础。
二、最小定制:实现__rich__方法
最简单的定制方式是实现__rich__方法。该方法的签名与约束如下:
- 不接受任何参数(只有
self); - 返回一个 Rich 能够渲染的对象,例如
Text、Table等; - 如果返回普通字符串,Rich 会将其按console markup(控制台标记语言)解析渲染,即字符串中的
[bold]、[cyan]之类的标记会被解释为样式。
官方文档给出的示例:
class MyObject: def __rich__(self) -> str: return "[bold cyan]MyObject()"此时打印或记录MyObject实例,终端会以粗体青色渲染出MyObject()。
当然,实际使用中__rich__可以返回更复杂的渲染对象。例如Foo类可以直接返回一个Text对象(tests/test_protocol.py):
from rich.text import Text class Foo: def __rich__(self) -> Text: return Text("Foo")console.print(foo)会输出Foo。测试还验证了Panel.fit(foo)能将协议对象嵌入面板中渲染(tests/test_protocol.py),说明__rich__返回的对象可以参与 Rich 内建渲染对象的组合。
__rich__与rich_cast()的底层机制
__rich__的实现入口是 rich/protocol.py 中的rich_cast()函数。它会在渲染前递归调用__rich__,把对象"降级"为 Rich 真正认识的渲染对象:
def rich_cast(renderable: object) -> "RenderableType": """Cast an object to a renderable by calling __rich__ if present.""" from rich.console import RenderableType rich_visited_set: Set[type] = set() # Prevent potential infinite loop while hasattr(renderable, "__rich__") and not isinstance(renderable, type): # Detect object which claim to have all the attributes if hasattr(renderable, _GIBBERISH): return repr(renderable) cast_method = getattr(renderable, "__rich__") renderable = cast_method() renderable_type = type(renderable) if renderable_type in rich_visited_set: break rich_visited_set.add(renderable_type) return cast(RenderableType, renderable)从源码结构可以归纳出三个关键细节:
- 递归降级:
__rich__返回的对象如果自身也实现了__rich__,会被继续调用,直到得到真正可渲染的对象。测试test_cast_deep(tests/test_protocol.py)构造了A() -> B() -> Foo()的链条,最终输出Foo。 - 无限循环防护:
rich_visited_set记录访问过的类型,一旦出现A() -> B() -> A()这种循环,立即中断。test_cast_recursive(tests/test_protocol.py)验证了循环场景下会退回__repr__输出。 - 防御"假协议对象":如果对象通过
__getattr__声称拥有所有属性,hasattr(renderable, _GIBBERISH)(_GIBBERISH是一段无意义的乱码字符串)会识破它并退回repr()。测试test_rich_cast_fake(tests/test_protocol.py)中的Fake类正是这种对象。
类型层面的协议定义
在类型系统层面,rich/console.py 用typing.Protocol定义了对应的结构化协议:
@runtime_checkable class RichCast(Protocol): """An object that may be 'cast' to a console renderable.""" def __rich__(self) -> Union["ConsoleRenderable", "RichCast", str]: ... @runtime_checkable class ConsoleRenderable(Protocol): """An object that supports the console protocol.""" def __rich_console__( self, console: "Console", options: "ConsoleOptions" ) -> "RenderResult": ...两者合并成类型别名RenderableType = Union[ConsoleRenderable, RichCast, str],即"一个字符串或任何可被 Rich 渲染的对象"。
三、进阶渲染:实现__rich_console__方法
__rich__的局限在于只能返回单个渲染对象。当需要更复杂的渲染(例如一次输出多段内容、按条件组合多个渲染块)时,应实现__rich_console__方法。
方法签名
__rich_console__接受两个参数:
console: Console:当前的Console实例,可用于查询终端宽度、调用console.render_str()等;options: ConsoleOptions:当前的控制台选项,包含max_width、min_width、height、style等渲染上下文信息(定义见 rich/console.py 附近的ConsoleOptions)。
方法应返回其他可渲染对象的可迭代集合(iterable)。返回类型即RenderResult = Iterable[Union[RenderableType, Segment]](rich/console.py)。
官方文档特别指出:虽然返回一个 list 之类的容器在语法上可行,但用yield语句实现为生成器(generator)通常更自然。
官方示例:Student 数据类
from dataclasses import dataclass from rich.console import Console, ConsoleOptions, RenderResult from rich.table import Table @dataclass class Student: id: int name: str age: int def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: yield f"[b]Student:[/b] #{self.id}" my_table = Table("Attribute", "Value") my_table.add_row("name", self.name) my_table.add_row("age", str(self.age)) yield my_table打印Student实例时,终端会先输出一行加粗的Student: #<id>,紧接着渲染出一张两列表格。注意这里yield的既有字符串(按 markup 解析),也有Table对象——RenderResult允许在同一个渲染序列中混合多种类型的渲染对象。
渲染管线中的调用位置
__rich_console__是渲染流程的真正枢纽。rich/console.py 中Console.render()的核心逻辑为:
renderable = rich_cast(renderable) if hasattr(renderable, "__rich_console__") and not isinstance(renderable, type): render_iterable = renderable.__rich_console__(self, _options) elif isinstance(renderable, str): text_renderable = self.render_str( renderable, highlight=_options.highlight, markup=_options.markup ) render_iterable = text_renderable.__rich_console__(self, _options) else: raise errors.NotRenderableError( f"Unable to render {renderable!r}; " "A str, Segment or object with __rich_console__ method is required" )由此可以推断出完整调用链:
- 对象先经过
rich_cast()递归处理(解决__rich__降级); - 若存在
__rich_console__,则调用它得到渲染序列; - 若为字符串,则先用
render_str()解析 markup 与高亮,再走 Text 的__rich_console__; - 两者都不是,则抛出
NotRenderableError。
值得注意:Rich 内建的几乎所有渲染对象(Text、Table、Panel、Layout、NewLine等)本身也是通过实现__rich_console__接入协议的。例如 rich/console.py 中的NewLine类:
class NewLine: """A renderable to generate new line(s)""" def __init__(self, count: int = 1) -> None: self.count = count def __rich_console__( self, console: "Console", options: "ConsoleOptions" ) -> Iterable[Segment]: yield Segment("\n" * self.count)可见协议并非"旁路",而是 Rich 渲染体系的统一内核。
四、底层渲染:yield Segment 实现完全控制
如果需要对终端输出拥有绝对控制权(例如逐段指定颜色、精确控制每个字符),可以在__rich_console__中直接 yieldSegment对象。
什么是 Segment
Segment由一段文本 + 一个可选的Style组成,是 Rich 渲染的最小单位(定义见 rich/segment.py)。官方文档给出的多色渲染示例:
from rich.console import Console, ConsoleOptions, RenderResult from rich.segment import Segment from rich.style import Style class MyObject: def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: yield Segment("My", Style(color="magenta")) yield Segment("Object", Style(color="green")) yield Segment("()", Style(color="cyan"))渲染MyObject实例时,"My" 显示为洋红色、"Object" 显示为绿色、"()" 显示为青色。这种逐段控制的能力,是__rich__和普通__rich_console__无法直接做到的。
为什么"越底层越可控"
Segment是渲染管线末端的产物——Console.render()的最终返回值就是Iterable[Segment]。因此直接 yieldSegment相当于绕过了所有中间层的样式封装,直接与渲染器对话。这也是自定义进度条、逐字符动画等场景的首选方式。除Segment(text, style)外,Style还支持更多属性(如bold、italic、underline、bgcolor等,参见 rich/style.py),可以组合出任意视觉效果。
五、测量宽度:实现__rich_measure__方法
有时 Rich 需要提前知道某个对象渲染后占用的字符宽度。例如Table在计算列宽时就需要测量每个单元格内容的宽度,以确定最优列宽。
方法签名与 Measurement
如果自定义对象没有使用 Rich 内建渲染对象,就必须自行提供__rich_measure__方法:
- 签名:
__rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement - 返回值:一个
Measurement对象,包含渲染所需字符数的最小值(minimum)与最大值(maximum)。
Measurement是定义在 rich/measure.py 中的NamedTuple:
class Measurement(NamedTuple): """Stores the minimum and maximum widths (in characters) required to render an object.""" minimum: int """Minimum number of cells required to render.""" maximum: int """Maximum number of cells required to render."""官方示例:国际象棋棋盘
官方文档以棋盘为例:渲染棋盘至少需要 8 个字符宽度(8 列棋子),最大值可以取当前可用最大宽度(假设棋盘居中显示):
from rich.console import Console, ConsoleOptions from rich.measure import Measurement class ChessBoard: def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement: return Measurement(8, options.max_width)这里options.max_width是ConsoleOptions提供的当前最大可用宽度,从源码结构看,它是渲染上下文的标准成员。
Measurement 的辅助方法
rich/measure.py 为Measurement提供了若干实用的推导方法,了解它们有助于写出健壮的测量逻辑:
| 方法 | 作用 |
|---|---|
span(属性) | 返回maximum - minimum,即宽度的波动范围 |
normalize() | 归一化,确保minimum <= maximum且minimum >= 0,负值被截断为 0 |
with_maximum(width) | 将最小/最大宽度都裁剪到不超过指定width |
with_minimum(width) | 将最小/最大宽度都抬升到不低于指定width(负数按 0 处理) |
clamp(min_width, max_width) | 组合应用with_minimum与with_maximum,把测量值夹在给定区间内 |
测量调用的源码流程
Measurement.get()(rich/measure.py)是测量的统一入口,其流程为:
- 若
options.max_width < 1(无空间),直接返回Measurement(0, 0); - 字符串先经
console.render_str()解析为Text; - 调用
rich_cast()降级对象; - 若对象实现了
__rich_measure__,调用之,并将结果经normalize()与with_maximum()处理; - 若对象没有
__rich_measure__,但可渲染,则退化为Measurement(0, max_width)(即最小 0、最大为可用宽度); - 否则抛出
NotRenderableError。
从测试用例可以佐证测量协议的实际应用:Table、Panel、Bar、Syntax、Text等均通过__rich_measure__提供宽度信息,例如 tests/test_text.py 验证了Text的测量结果,tests/test_syntax.py 验证了代码块在不同宽度下的测量值。
何时需要实现__rich_measure__
结合 measure.py 的退化分支可以推断:如果你的对象不参与需要宽度计算的布局(如独立的console.print()单行输出),可以省略__rich_measure__,Rich 会按Measurement(0, max_width)处理。但一旦对象要被Table包裹、或与Columns、Layout等布局类组合使用,缺失__rich_measure__就会导致宽度估计失准,此时务必实现该方法。
六、协议的综合运用:完整的自定义渲染类
将前文三种协议方法组合起来,可以得到一个同时具备"渲染能力"与"测量能力"的完整自定义类:
from rich.console import Console, ConsoleOptions, RenderResult from rich.measure import Measurement from rich.segment import Segment from rich.style import Style class StatusBadge: """一个支持 Console Protocol 的自定义状态徽章。""" def __init__(self, label: str, ok: bool) -> None: self.label = label self.ok = ok def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: color = "green" if self.ok else "red" mark = "✔" if self.ok else "✘" yield Segment(f"[ {mark} ", Style(color=color)) yield Segment(self.label, Style(bold=True)) yield Segment(" ]", Style(color=color)) def __rich_measure__( self, console: Console, options: ConsoleOptions ) -> Measurement: # 最小宽度 = 标签长度 + 装饰符;最大宽度同理(单行不换行) width = len(self.label) + 6 return Measurement(width, width) console = Console() console.print(StatusBadge("services", ok=True)) console.print(StatusBadge("database", ok=False))这里同时用到了Segment逐段着色和__rich_measure__精确测宽,可直接复制运行验证效果。
七、用RichRenderable做协议的类型检查
rich/abc.py 提供了一个基于ABC的虚拟基类RichRenderable,用于类型层面的协议检查。它没有强制要求继承,而是通过__subclasshook__自动识别实现了协议方法的类:
class RichRenderable(ABC): """An abstract base class for Rich renderables. Note that there is no need to extend this class, the intended use is to check if an object supports the Rich renderable protocol. For example:: if isinstance(my_object, RichRenderable): console.print(my_object) """ @classmethod def __subclasshook__(cls, other: type) -> bool: """Check if this class supports the rich render protocol.""" return hasattr(other, "__rich_console__") or hasattr(other, "__rich__")使用方式:
from rich.abc import RichRenderable if isinstance(obj, RichRenderable): console.print(obj)注意其判定标准是实现了__rich_console__或__rich__即可。测试 tests/test_protocol.py 验证了:实现了__rich__的Foo、以及Text、Panel实例都是RichRenderable,而普通字符串和 list不是。
八、协议对比与选型建议
三种协议方法适用场景各不相同,梳理如下:
| 协议方法 | 参数 | 返回 | 适用场景 | 局限性 |
|---|---|---|---|---|
__rich__ | 无 | 单个可渲染对象(Text/Table/字符串等) | 快速接入、对象整体替换为另一种渲染 | 只能返回单个对象,无法多段组合 |
__rich_console__ | console、options | 可渲染对象或Segment的迭代器 | 复杂组合渲染、条件分支、多块输出 | 需要理解ConsoleOptions与渲染上下文 |
__rich_measure__ | console、options | Measurement | 参与Table/Columns/Layout等宽度敏感布局 | 单行独立输出时可省略 |
实践建议:
- 想让对象"看起来像"某个内建渲染对象(如始终渲染为一张表),优先用
__rich__; - 需要一次输出多块内容、或根据运行条件动态渲染,用
__rich_console__+yield; - 需要逐字符控制颜色或做动画,用
__rich_console__+Segment; - 只要对象会被嵌入宽度敏感容器,务必补上
__rich_measure__。
九、小结
Console Protocol 是 Rich 生态的扩展基石:__rich__提供最简接入,__rich_console__提供灵活的组合渲染与Segment级底层控制,__rich_measure__为宽度敏感的布局提供度量信息。它们共同由 rich/protocol.py 的is_renderable()/rich_cast()驱动,由 rich/console.py 的Console.render()统一调度,并由 rich/abc.py 的RichRenderable提供运行时类型检查——这套机制不仅服务于内建的Text、Table、Panel,也让任何第三方对象都能以第一方身份融入 Rich 的渲染管线。
掌握了这三种协议方法,你就可以为自己的数据模型、领域对象乃至整个应用层定制专属的终端可视化方案,让调试信息一目了然、让命令行工具的输出层次分明。
【免费下载链接】richRich is a Python library for rich text and beautiful formatting in the terminal.项目地址: https://gitcode.com/gh_mirrors/ri/rich
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考