• 下面我们来看render(renderable, render_options)函数的渲染逻辑,该函数里会调用下面的代码

    render_iterable = renderable.__rich_console__(self, options)
    

    在函数声明里renderable对象是RenderableType类型的,但实际上Text类型的,并且这两种类型没有继承关系,这里没太想明白作者为什么这样搞。所以,这里的__rich_console__函数我们要到text.py文件中去找。__rich_console__函数最终会调用Text对象的render函数,核心代码如下:

    def render(self, console: "Console", end: str = "") -> Iterable["Segment"]:
      style_map = {index: get_style(span.style) for index, span in enumerated_spans}
    
      _Segment = Segment
    
      for (offset, leaving, style_id), (next_offset, _, _) in zip(spans, spans[1:]):
        yield _Segment(text[offset:next_offset], get_current_style())
    

    调用get_style函数,将格式转为Style对象,如:'bold red'转成Style对象,然后按照不同的显示格式进行‘分片’,每个‘片段’构造一个Segment对象存储文本及其对应的格式。

    get_style函数会调用Style.parse(name)生成Style对象,核心代码如下

    @lru_cache(maxsize=1024)
    def parse(cls, style_definition: str) -> "Style":
      words = iter(style_definition.split())
      for original_word in words:
        word = original_word.lower()
        if word == "on":
          # ...省略
        elif word in style_attributes:
          attributes[style_attributes[word]] = True
        else:
          color = word
      style = Style(color=color, bgcolor=bgcolor, link=link, **attributes)
      return style
    

    参数style_definition取值为bold red,分割后生成['bold', 'red']列表,当word变量等于'bold'时,会执行attributes[style_attributes[word]] = True语句,执行后attributes等于{'bold': true},它是一个字典。当word变量等于red时,执行color=word语句。最终调用导数第二行构造Style对象,Style对象最核心的两个数据形式_attributes_color, 前者是int类型,在我们例子中取值是1,代表'bold',即:粗体。后者代表颜色,即:'red',它是Color类型的,该类中有个属性number也是我们后续要用到的。

    下面来看下__rich_console__函数返回了哪些Segment对象

    可以看到有4个,每一个都有文本及其Style对象。

    回到render(renderable, render_options)函数,刚刚介绍了__rich_console__部分,下面还有返回的代码, 一起来看看

    iter_render = iter(render_iterable)
    for render_output in iter_render:
      if isinstance(render_output, Segment):
        yield render_output
    

    render_iterable变量是__rich_console__的返回值,即:4个Segment对象。遍历后通过yield方式返回。该关键字用来返回一个迭代器,也可以理解为一个列表。并且yield返回有个特点,函数返回值只有真正被使用的时候才会执行调用函数。

    这样,render(renderable, render_options)函数就讲解完了,返回上一层extend(render(renderable, render_options)),通过extend函数将4个Segment对象保存到buffer中,结果如下

    然后print方法就执行完了。看起来已经结束了,然而控制台打印的代码貌似没有看到。答案就在刚刚的with self中,with关键字使得执行完代码体后,会自动调用self__exit__函数。__exit__函数中调用_render_buffer函数进行最终的输出,核心代码如下

    output: List[str] = []
    append = output.append
    for line in Segment.split_and_crop_lines(buffer, self.width, pad=False):
        for text, style, is_control in line:
            if style and not is_control:
                append(
                    style.render(
                        text,
                        color_system=color_system,
                        legacy_windows=legacy_windows,
                    )
                )
    rendered = "".join(output)
    
    return rendered
    

    split_and_crop_lines函数是为了适应控制台的宽度,暂时忽略它。line变量仍然是刚刚提到的4个Segment对象,通过for text, style, is_control in line直接将每个Segment对象的属性解出来并赋给text, style, is_control变量,最终每个style对象都会调用render方法完成最后的渲染。

    render方法核心代码如下

    attrs = self._make_ansi_codes(color_system)
    rendered = f"\x1b[{attrs}m{text}\x1b[0m" if attrs else text
    

    _make_ansi_codes函数就不展开了, 其实就是利用上面提到的_attributesnumber属性生成标准输出的能够识别的格式,返回值attrs的结果为1;31,1取自_attributes代表粗体,31中的1取自number代表颜色,其他颜色取值是不同的,比如黄色是33,紫色是35。最后通过f-string格式(新特性)生成rendered变量,取值为World它就是标准输出流能够识别的格式。

    回到_render_buffer函数中,调用rendered = "".join(output)将4个渲染后的片段拼在一起,返回。返回后执行的代码如下:

    text = self._render_buffer()
    if text:
        self.file.write(text)
    

    self.file变量的赋值语句为self.file = file or sys.stdout,由于我们没有定义file变量,所以self.file取值为sys.stdout。最终的输出为sys.stdout.write(text),至此整个流程就讲解完了。如果你理解了上述逻辑,应该可以通过下面代码输出同样的效果

    sys.stdout.write('Hello, \033[1;31mWorld\033[0m!')
    

    所以Rich做的就是把文字格式准成标准输出流能识别的格式。

    Rich里用到的代码确实挺新的,能学到很多东西,比直接看书来的快,有兴趣的朋友可以自行阅读。欢迎关注公众号**渡码**不断分享优秀开源项目源码分析

    Python优秀开源项目Rich源码解析-LMLPHP
    07-06 15:45