Python 优秀开源项目 Rich 源码解析
source link: http://mp.weixin.qq.com/s?__biz=MzUyOTk2MTcwNg%3D%3D&%3Bmid=2247485757&%3Bidx=1&%3Bsn=ee2ca7134d412da621104b11f1af323e
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
:point_up_2: “ Python猫 ” ,一个值得加星标的 公众号
剧照|《三国机密之潜龙在渊》
来源:渡码@渡码公众号
这篇文章对优秀的开源项目 Rich
的源码进行解析, OMG,盘他 。为什么建议阅读源码,有两个原因,第一,单纯学语言很难在实践中灵活应用,通过阅读源码可以看到每个知识点的运用场景,印象会更深,以后写代码的时候就能应用起来;第二,通过阅读优秀的开源代码,可以学习比人的代码规范、设计思路;第三,参与到开源社区,获得更广阔的的发展前景;第四,面试加分项。所以,有时间的话还是建议大家多读读优秀开源项目的源码。
下面进入今天的主题,这个开源项目的名字叫 Rich
,将近8k star,地址:https://github.com/willmcgugan/rich (可以点击文末 阅读原文
查看)。这个项目是个英国老铁开发的,比较友好的是有中文文档。它的作用是可以在控制台输出富文本和精美的可视化格式(如:表格、进度条和markdown)。截图感受一下
进度条
效果看起来很酷炫,我忍不住看了一些代码,发现作者用的是 Python
3.8版本实现的,好多新特性我也不了解,所以在看源码过程中还补了一下语法基础。下面以一个例子来简单看看 Rich
的源码,源码的讲解我尽量言简意赅,重点讲解源码中涉及的一些关键的知识点。
先捡个软柿子捏,如下:
from rich import print print('Hello, [bold yellow]World[/bold yellow]!')
输出效果:
可以看到对单词 World
显示为粗体、红颜色。
先通过一张图来看看大致流程
简单来说就是将文本的格式转化成标准输出能够识别的格式,然后输出即可。下面来讲解源码,当我们调用 print
函数时,最终程序会跳转到 console.py
文件的 print
函数中,执行以下代码
调用 self._collect_renderables
函数处理输入的字符串,将需要格式化的部分标出来,返回的 renderables
变量是一个 Text
列表,因为输入只有1个字符串,所以列表的大小为1,变量结果如下
Span(7, 12, 'bold red')
便是框出来需要格式化的内容。
上述代码还有一个 with self
,它的作用我们一会儿再说。接着 print
函数往下看
这里会遍历刚刚提到的 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
对象
可以看到有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
函数就不展开了, 其实就是利用上面提到的 _attributes
和 number
属性生成标准输出的能够识别的格式,返回值 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
里用到的代码确实挺新的,能学到很多东西,比直接看书来的快,有兴趣的朋友可以自行阅读。经常读我文章的朋友知道,我一直在寻找新的内容、新方向,这次源码解析也是一次新的尝试,不知道是不是一件有价值的事情,先持续更新几篇看看。如果你觉得有用也想看更多的源码解析的文章,希望点个赞或者在看鼓励一下,不胜感激。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK