Python 优秀开源项目 Rich 源码解析

栏目: IT技术 · 发布时间: 4年前

内容简介:剧照|《三国机密之潜龙在渊》

:point_up_2:  Python猫 ” ,一个值得加星标的 公众号

Python 优秀开源项目 Rich 源码解析

剧照|《三国机密之潜龙在渊》

来源:渡码@渡码公众号

这篇文章对优秀的开源项目 Rich 的源码进行解析, OMG,盘他 。为什么建议阅读源码,有两个原因,第一,单纯学语言很难在实践中灵活应用,通过阅读源码可以看到每个知识点的运用场景,印象会更深,以后写代码的时候就能应用起来;第二,通过阅读优秀的开源代码,可以学习比人的代码规范、设计思路;第三,参与到开源社区,获得更广阔的的发展前景;第四,面试加分项。所以,有时间的话还是建议大家多读读优秀开源项目的源码。

下面进入今天的主题,这个开源项目的名字叫 Rich ,将近8k star,地址:https://github.com/willmcgugan/rich (可以点击文末 阅读原文 查看)。这个项目是个英国老铁开发的,比较友好的是有中文文档。它的作用是可以在控制台输出富文本和精美的可视化格式(如:表格、进度条和markdown)。截图感受一下

Python 优秀开源项目 Rich 源码解析
各种格式

Python 优秀开源项目 Rich 源码解析

进度条

效果看起来很酷炫,我忍不住看了一些代码,发现作者用的是 Python 3.8版本实现的,好多新特性我也不了解,所以在看源码过程中还补了一下语法基础。下面以一个例子来简单看看 Rich 的源码,源码的讲解我尽量言简意赅,重点讲解源码中涉及的一些关键的知识点。

先捡个软柿子捏,如下:

from rich import print

print('Hello, [bold yellow]World[/bold yellow]!')

输出效果:

可以看到对单词 World 显示为粗体、红颜色。

先通过一张图来看看大致流程

Python 优秀开源项目 Rich 源码解析

简单来说就是将文本的格式转化成标准输出能够识别的格式,然后输出即可。下面来讲解源码,当我们调用 print 函数时,最终程序会跳转到 console.py 文件的 print 函数中,执行以下代码

Python 优秀开源项目 Rich 源码解析

调用 self._collect_renderables 函数处理输入的字符串,将需要格式化的部分标出来,返回的 renderables 变量是一个 Text 列表,因为输入只有1个字符串,所以列表的大小为1,变量结果如下

Python 优秀开源项目 Rich 源码解析

Span(7, 12, 'bold red') 便是框出来需要格式化的内容。

上述代码还有一个 with self ,它的作用我们一会儿再说。接着 print 函数往下看

Python 优秀开源项目 Rich 源码解析

这里会遍历刚刚提到的 renderables 变量,先调用 render 函数渲染输入的文本,然后调用 extend 函数将 render 返回的结果添加到 self._buffer 列表里。这里有几个知识点简单说一下

  • self._buffer
    @property
    self._thread_locals.buffer
    List[Segment]
    
  • self._thread_locals.buffer
    dataclasses
    field
    buffer: List[Segment] = field(default_factory=list)
    dataclasses
    Python
    field
    @dataclass
    __init__
    
  • extend = self._buffer.extend
    list
    extent
    extend
    对象名.extend
    

下面我们来看 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 对象

Python 优秀开源项目 Rich 源码解析

可以看到有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 中,结果如下

Python 优秀开源项目 Rich 源码解析

然后 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 变量,取值为 [1;31mWorld[0m 它就是标准输出流能够识别的格式。

回到 _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 里用到的代码确实挺新的,能学到很多东西,比直接看书来的快,有兴趣的朋友可以自行阅读。经常读我文章的朋友知道,我一直在寻找新的内容、新方向,这次源码解析也是一次新的尝试,不知道是不是一件有价值的事情,先持续更新几篇看看。如果你觉得有用也想看更多的源码解析的文章,希望点个赞或者在看鼓励一下,不胜感激。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hacker's Delight

Hacker's Delight

Henry S. Warren Jr. / Addison-Wesley / 2002-7-27 / USD 59.99

A collection useful programming advice the author has collected over the years; small algorithms that make the programmer's task easier. * At long last, proven short-cuts to mastering difficult aspec......一起来看看 《Hacker's Delight》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码