這篇文章對優秀的開源項目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)
,
dataclasses
是
Python
3.7 版本的新引入的模塊,
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
裏用到的代碼確實挺新的,能學到不少東西,比直接看書來的快,有興趣的朋友能夠自行閱讀。歡迎關注公衆號**渡碼**不斷分享優秀開源項目源碼分析