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

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

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

各类格式
各类格式

进度条

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

先捡个软柿子捏,以下:面试

from rich import print
 print('Hello, [bold yellow]World[/bold yellow]!') 复制代码

输出效果:windows

能够看到对单词World显示为粗体、红颜色。markdown

先经过一张图来看看大体流程app

简单来讲就是将文本的格式转化成标准输出可以识别的格式,而后输出便可。下面来说解源码,当咱们调用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)dataclassesPython 3.7 版本的新引入的模块, field函数可提供更加灵活的初始化方式,而且该模块中的 @dataclass注解能够为类自动添加 __init__等方法,比较方便
  • extend = self._buffer.extend这种写法将 listextent函数存到了临时变量里,后续直接经过 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函数就不展开了, 其实就是利用上面提到的_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里用到的代码确实挺新的,能学到不少东西,比直接看书来的快,有兴趣的朋友能够自行阅读。欢迎关注公众号**渡码**不断分享优秀开源项目源码分析

相关文章
相关标签/搜索