فهرست مطالب

"Python rich library

مقدمه🔗

کتابخانه‌ی rich ابزار نیرومندی در زبان پایتون است برای نمایش ویژگی‌های ‌rich در ترمینال! مثلا با آن می‌توان:

  • متن‌ها را رنگ‌بندی کرد.
  • متن‌ها را ‌bold، ‌italic و یا زیر خط دار کرد.
  • متن‌ها را چپ‌چین، راست‌چین و یا وسط‌چین کرد.
  • متن‌ها را به یک آدرس اینترنتی لینک کرد.
  • جدول ساخت و اطلاعات را در جدول نمایش داد.
  • source code ها را بسته به زبان برنامه‌نویسی به صورت syntax highlight شده نمایش داد.
  • کدهای Markdown را به صورت تفسیر شده نمایش داد.
  • صفحه‌ی ترمینال را لایه بندی کرد و در هر لایه چیزی قرار داد.
  • progress bar ساخت و وضعیت پیشرفت یک یا چند عملیات را به صورت بصری به کاربر نشان داد.
  • tree ساخت و اطلاعات را به صورت شاخه شاخه و تو در تو نمایش داد.

rich ابزار فوق‌العاده نیرومندی است. بعد از آشنایی با آن، راز ترمینال‌هایِ زیبایی که معمولا در هنگام نصب پکیج‌های نرم‌افزاری می‌بینید برایتان فاش خواهد شد و دیگر چندان با اعجاب به آن‌ها نگاه نخواهید کرد، برای اینکه خود قادر به نوشتن چنین برنامه‌هایی خواهید بود.

از حاشیه‌ها کم کنم و به اصل مطلب بپردازم. در این پست، ابتدا چند کلاسِ اصلی این کتابخانه را معرفی و با تکه کدهای کوچک، عملکرد آن‌ها را به صورت تکی بررسی می‌کنیم و سپس در ادامه پروژه‌ای کوچک تعریف و با مطالب آموخته، شده آن را پیاده‌سازی خواهیم کرد.

قبل از هر چیز می‌بایست rich را نصب کرد. نصب کتابخانه‌ی rich راحت و سر راست است و با دستور pip به صورت زیر قابل انجام است:

SHELL
$ pip install rich

اگر با pip آشنایی ندارید و یا علاقمند به مرور دانسته‌های خود هستید، پست در مورد pip را بخوانید.

کلاس Prompt🔗

معرفی کلاس‌ها را از کلاس Prompt شروع می‌کنیم که با آن از کاربر تقاضا می‌کنیم که اطلاعی را وارد کند تا برنامه آن را بخواند. کد زیر نحوه‌ی import و استفاده از این کلاس را نشان می‌دهد:

prompt_1.py
from rich.prompt import Prompt
name = Prompt.ask("Enter your name")
print("You entered: ", name)

می‌توان به آن مقدار پیش‌فرض هم داد؛ یعنی اگر کاربر چیزی وارد نکند و مستقیما دگمه‌یEnter را بزند برنامه مقداری که ما تعیین کرده‌ایم را بر می‌گرداند. حتی می‌توان به آن لیستی از مقادیر را داد و کاربر را مجبور کرد که فقط، یکی از مقادیر مشخص شده را وارد کند. در این صورت rich چرخه‌ی دریافت دیتا را تا زمانی که مقدار صحیحی وارد نشده است تکرار می‌کند. مقدار پیش فرض به وسیله‌ی default keyword argument و لیست مقادیر قابل قبول توسط choices keyword argument مشخص می‌شود. کد زیر این موضوع را نشان می‌دهد:

prompt_2.py
from rich.prompt import Prompt
from rich.console import Console

name = Prompt.ask("Enter a valid name", default="Sara", choices=["Sara", "Mohsen", "Nima", "Ali"])

console = Console()
console.print(f"name: [red bold]{name}")

نمونه خروجی کد فوق را در تصویر زیر می‌بینید:

"rich.prompt"

کلاس Confirm🔗

Confirm در واقع یک نوع Prompt خاص است و وظیفه‌ دارد که کاربر را مجبور به وارد کردن y یا n کند. خروجی آن True یا False است.

confirm_1.py
from rich.prompt import Confirm

response = Confirm.ask("به نظر شما این پست مفید است؟")
print(response)

نمونه خروجی کد بالا به صورت زیر است:

"python rich confirm"

کلاس Padding🔗

کلاس Padding برای انداختن فضای خالی در اطراف متن و یا هر چیزی که توسط rich قابل render شدن باشد استفاده می‌شود. n فضای خالی در بالا و پایین یک renderable به معنای n خط خالی است و n فضای خالی در سمت چپ و راست آن به معنای n کاراکتر Space است.

قبل از توضیح بیشتر کد زیر را ببینید:

rich_padding_1.py
from rich.console import Console
from rich.padding import Padding
console = Console()
widget = Padding("My name is Mohsen", 2)
console.print(widget)

خروجی کد بالا به صورت زیر است. به دو خط خالی بالای متن دقت کنید. اگر چه شاید واضح نباشد ولی دو کاراکتر Space هم در ابتدای خطی که «نوشته» است وجود دارد.

"rich Padding class"

اگر فضای خالی اطراف renderable یکسان نباشد به جای آرگومان دوم می‌توانیم از یک tuple استفاده کنیم. اگر tuple حاوی دو عدد باشد عدد اول به معنای تعداد خط خالی در بالا و پایین و عدد دوم به معنای تعداد کاراکتر Space در سمت چپ و راست renderable است. اگر tuple حاوی چهار عدد باشد به ترتیب نشان دهنده‌ی تعداد خط خالی در بالا، تعداد کاراکتر Space در سمت راست، تعداد خط خالی در پایین و تعداد کاراکتر Space در سمت چپ renderable است. اگر با css آشنا باشید این شیوه‌ی مقدار دهی برایتان آشناست.

کلاس Padding یک keyword argument به نام expand دارد که مقدار True یا False می‌گیرد. به طور پیش‌فرض مقدار آن True است و به این معنی است که از کل عرضِ «در دسترس» استفاده خواهد کرد. می‌توانید آن را False کنید تا فقط به اندازه‌ی مورد نیاز فضا اشغال کند. کد زیر را ببینید:

rich_padding_2.py
from rich.console import Console
from rich.padding import Padding
console = Console()
widget = Padding("My name is Mohsen", (2, 1), expand=False)
console.print(widget)

کد فوق را اجرا می‌کنیم و خروجی را به برنامه‌ی sed می‌فرستیم تا تمام کاراکترهای Space را با _ پر کند. می‌بینید که در ابتدا و انتهای نوشته یک کاراکتر Space قرار دارد و کل فضای اختصاص داده شده به این widget به اندازه‌ی مقدار متن است. نکته‌ی قابل توجه اینکه خط‌های خالی فقط شامل کاراکتر \n نیستند بلکه با کاراکترهای Space پر شده اند.

"rich padding example"

کلاس Table🔗

Table یا جدول، ابزار فوق‌العاده‌ای برای نمایشِ قابل فهم اطلاعات است و چه خوب است که در ترمینال هم بتوانیم جدول بسازیم. ساده‌ترین جدول دارای تعدادی ستون و تعدادی سطر است. بعد از import کردن کلاس Table در برنامه و ساخت یک instance از آن، دو متد در اختیار ما قرار می‌گیرد، یکی به نام add_column و دیگری به نام add_row. با این دو متد و سپس print کردن آبجکت table در کنسول، جدول به نمایش درمی‌آید.

کد زیر اطلاعات اعضای یک خانواده را نمایش می‌دهد:

table_members_of_a_family.py
from rich.console import Console
from rich.table import Table

table = Table(title="Mr Alavi's Family",caption="This is a small family", title_style="bold red")

table.add_column("Name", style="blue")
table.add_column("Family", style="yellow")
table.add_column("Role")
table.add_column("Age")

table.add_row("Ali", "Alavi", "Father", "59")
table.add_row("Sara", "Saravi", "Mother", "53", style="bold red")
table.add_row("Mohammad", "Alavi", "Son", "35")
table.add_row("Jalal", "Alavi", "Son", "30")
table.add_row("Fateme", "Alavi", "Daughter", "28")

console = Console()
console.print(table)

خروجی کد فوق به صورت زیر است:

"rich table"

به دو خط highlight شده توجه کنید. در خط چهارم، در تابع constructor کلاس Table می‌توانیم keyword argumentهای زیادی را استفاده کنیم. اینجا از سه kwarg استفاده کرده‌ام. title عنوان جدول را در بالای آن نمایش می‌دهد. title_style ویژگی‌های نمایشی title را ست می‌کند. در اینجا گفته‌ام که title را bold و به رنگ قرمز کند. caption توضیحی اضافه است و در پایین جدول نشان داده می‌شود. بعد از ایجاد جدول باید آن را ‌print کنیم. برای این منظور آبجتی از کلاس Console می‌سازیم و سپس متد print آن را با آبجکت جدول صدا می‌زنیم. این کاری است که در دو خط آخر کرده‌ایم.

یک بار دیگر تصویر فوق را نگاه کنید. فضایی که rich به جدول اختصاص داده است دقیقا به اندازه‌ی مورد نیاز جدول است. اگر می‌خواهید از تمام عرض در دسترس استفاده کنید expand=True را در constructor کلاس Table اضافه کنید.

کلاس Columns🔗

آیا تا به حال به خروجی دستور ls در خط فرمان سیستم عامل‌های شبه یونیکس توجه کرده‌اید که چگونه بسته به عرض ترمینال خروجی را در تعداد ستون‌های متغیر، اما مرتب و منظم نشان می‌دهد؟ ایجاد این نوع خروجی در rich به سادگی استفاده از کلاس Columns است. این کلاس به عنوان آرگومان نخست یک iterable می‌گیرد. اگر از keyword arguments هایی که می‌توانیم استفاده کنیم صرف نظر کنیم تنها کافیست که آبجکت حاصله را به متد print آبجکتی از کلاس Console بدهید. سخت نیست! مثال زیر را ببینید:

columns_1.py
from rich.console import Console
from rich.columns import Columns
from urllib.request import urlopen
import json

results = urlopen("https://randomuser.me/api?results=30&nat=us")
records = json.loads(results.read())["results"]
names = []

for record in records:
    names.append("<" + record['name']['first'] + " " + record['name']['last'] + ">")

columns = Columns(names)

console = Console()
console.print(columns)

از سایت randomuser.me سی رکورد فیک می‌خوانیم و نام و نام‌خانوادگی آنها را استخراج و در یک لیست می‌گذاریم. برای اینکه حدود متن را به درستی متوجه شویم هر کدام را بین <> محصور می‌کنیم تا ببینیم که آیا rich ستون‌ها را به درستی تنظیم کرده است یا نه. یک نمونه از خروجی برنامه را در زیر می‌بینید. خروجی برنامه برای شما فرق خواهد داشت، برای اینکه رکوردها هر بار به صورت رندوم و فیک تولید می‌شوند.

"rich column in python"

به نظر می‌رسد که rich کارش را درست انجام داده است!

کلاس Tree🔗

Tree مفهوم پیچیده‌ای نیست اما شاید به نظر برسد که پیاده‌سازی آن سخت است ولی با rich این گونه نیست. سخت‌ترین نمودارهای درختی و تو در تو را می‌توانیم به راحتی با کلاس Tree ایجاد کنیم. باز هم یادآور می‌شوم که در نهایت درخت ایجاد شده باید به متد print آبجکتی از کلاس Console فرستاده شود تا در صفحه به نمایش درآید.

tree_1.py
from rich.console import Console
from rich.tree import Tree

tree = Tree("/")
bin_ = tree.add("bin")
home = tree.add("home", guide_style="underline2")

bin_.add("cat", style="overline")
bin_.add("ls")
bin_.add("[italic cyan]awk")

home.add("mohsen", style="red bold").add("mohsen_documents")
home.add("ehsan").add("ehsan_documents")

tree.add("media")
tree.add("cdrom")

console = Console()
console.print(tree)

راز توانایی بالای rich در ساختن درخت در متد add نهفته است. این متد شاخه‌ی جدید می‌سازد و یک درخت جدید بر می‌گرداند، به صورتی که اگر متد add درخت برگشتی را صدا کنید شاخه جدید به عنوان فرزند آن ساخته می‌شود. به عنوان مثال در خط پنجم شاخه‌ای به نام bin در درخت اصلی می‌سازیم و آبجکت برگشتی را در متغیری به نام _bin ذخیره می‌کنیم. در خطوط هشت و نه و ده متد add این آبجکت را صدا میزنیم و شاخه‌هایی که ایجاد می‌شوند به عنوان فرزندان شاخه‌ی bin منظور می‌شوند.

نکته‌ی دیگر در مورد style keyword argument و guide_style keyword argument است. اگر شاخه‌ای زیر شاخه داشته باشد استایل داده شده بر تمام زیر شاخه‌ها هم اعمال می‌شود. این موارد را در شکل زیر به خوبی مشاهده می‌کنید.

"rich tree python"

کلاس Syntax🔗

شاید زمانی فکر کرده باشید که چه خوب می‌شد اگر می‌توانستیم در ترمینال source codeها را به صورت syntax highlight شده ببینیم. من به این موضوع فکر کرده بودم و راستش زیاد پیگیرش نشده بودم تا اینکه در rich با آن مواجه شدم و از سادگی و سر راستی‌اش یکه خوردم. کافیست آبجکتی از کلاس Syntax بسازید و source code را به عنوان اولین آرگومان و نامِ زبانِ برنامه‌‌ را عنوان دومین آرگومان به آن بدهید و آبجکت دریافتی را در console پرینت کنید.

بیایید برنامه‌ای بنویسیم که نام و زبان یک فایل را از خط فرمان بگیرد و محتوایش را به صورت syntax highlight شده در ترمینال نمایش دهد.

syntax_1.py
from rich.console import Console
from rich.syntax import Syntax
from rich.padding import Padding
import sys

if len(sys.argv) != 3:
    print(f"usage: {sys.argv[0]} filename file-language", file=sys.stderr)
    sys.exit(1)

console = Console()

try:        
    with open(sys.argv[1], "r") as file:
        syntax = Syntax(file.read(), sys.argv[2], padding=(1, 0), line_numbers=True)
        console.print(Padding(syntax, (1, 0)))
except Exception as e:
    print(e)

به keyword argumentهای استفاده شده در constructor کلاس Syntax توجه کنید. با استفاده از line_numbers=True خط‌ها را شماره‌گذاری می‌کنیم و با padding=(1, 0) یک خط خالی در بالا و پایین داخل کادر کد ایجاد میکنیم (در تصویر پایین این قسمت با رنگ پس زمینه مشخص است.) از آنجایی که به خاطر وجود شماره‌ی خط‌ها سمت چپ دارای فضای کافی است در چپ و راست نیاز به فضای خالی نداریم لذا عدد مربوطه را صفر می‌گذاریم. موقع پرینت آبجکت با استفاده از کلاس Padding یک خط خالی در بالا و پایین کادر کد ایجاد می‌کنیم تا ظاهر کار زیباتر و تمرکز بر کد ‌highlight شده بیشتر شود.

حالا به عنوان مثال کد همین برنامه را syntax highlight می‌کنیم:

"rich syntax python"

کلاس Markdown🔗

با rich می‌توان حتی کدهای Markdown را هم در ترمینال به صورت تفسیر شده نشان داد. کافیست از کلاس Markdown یک آبجکت بسازید. کد «Markdown» را باید به عنوان اولین آرگومان به تابع constructor بفرستید و بعد آبجکت حاصله را در console پرینت کنید. همان طور که احتمالا تا الان متوجه شده‌اید این شیوه‌ایست که در مورد بسیاری از کلاس‌ها به کار می‌رود. مثلا در مورد کلاس‌های Syntax و Tree هم روند کار دقیقا به همین صورت بود.

برنامه‌ی زیر نام یک فایل Markdown را از خط فرمان می‌خواند و آن را به صورت render شده در ترمینال نمایش می‌دهد.

markdown_1.py
from rich.console import Console
from rich.markdown import Markdown
import sys

console = Console()

try:
    file = open(sys.argv[1], "r")
    md = Markdown(file.read())
    console.print(md)
except Exception as e:
    print(e, file=sys.stderr)

اگر فایل Markdown برای تست ندارید این فایل را دانلود کنید. اگر برنامه را با این فایل فراخوانی کنیم خروجی به صورت زیر خواهد بود:

"python rich markdown"

توجه کنید که علاوه بر تمام موارد نظیر رندر شدن دقیق لینک‌ها، بلاکِ کد به صورت کامل syntax highlight شده است.

کلاس Status🔗

این ویدیو را ببینید تا متوجه شوید در مورد چه چیزی صحبت می‌کنیم.

خُب! پیداست که کاربرد آن کجاست. برای ساختن چنین چیزی باید از کلاس Status استفاده کنیم. تابع constructor این کلاس یک متن یا یک renderable می‌گیرد و یک context manager بر‌می‌گرداند. اگر نمی‌دانید context manager چیست به طور خلاصه بگویم که کلاسی که پروتوکل context manager را پیاده‌سازی کرده باشد را می‌توان در عبارت with پایتون استفاده کرد1.

کد زیر را ببینید تا بعد در مورد آن بیشتر صحبت کنیم. ویدیوی بالا در واقع خروجی این کد است:

status_1.py
from rich.console import Console
import time

console = Console()
console.print("[bold red]Start doing jobs")

with console.status("Please waiting...") as status:
    for i in range(5):
        console.log(f"job number #{i}")
        time.sleep(1)

console.print("[bold blue]Jobs done") 

در خط highlight شده، console.status("Please waiting...") یک آبجکت از کلاس Status برمی‌گرداند. می‌توانستیم این کار را نکنیم و مستقیما و به صورت زیر آبجکتی از کلاس Status بسازیم:

status_1.py
# ...
from rich.status import Status
# ...
with Status("Please waiting...") as status:
# ...

spinner را می‌توان توسط ‌kwarg ای به همین نام تغییر داد.

کلاس Progress🔗

یک شیوه‌ی ساخت progress bar استفاده از تابع track است. ابتدا باید این تابع را import کرد و سپس آن را با یک iterable فراخواند. track هر بار یک عنصر از این iterable را بر می‌گرداند و از آنجایی که تعداد عناصر را با استفاده از تابع len محاسبه کرده دقیقا می‌داند که چند عنصر را برگردانده و چند عنصر باقی مانده است و به این ترتیب می‌تواند یک ‌progress bar بسازد.

progress_1.py
from rich.progress import track
from time import sleep

for i in track(range(100), description="Processing data"):
    sleep(0.05)

برای دیدن خروجی برنامه باید از آن ویدئو ضبط کرد ولی در این جا به عکسی کفایت کنید!

"python rich progress"

اما گاهی اوقات لازم است که چندین progress bar داشته باشیم. مثلا یکی وضعیتِ پیشرفتِ دانلود را اطلاع دهد و دیگری وضعیت پردازش اطلاعات دانلود شده را. برای این منظور لازم است مستقیما از کلاس Progress استفاده کنیم.

هر progress bar را به عنوان یک task می‌شناسیم و به تعداد دلخواه می‌توانیم task اضافه کنیم. کلاس Progress به صورت context manager 1 طراحی شده است لذا می‌توانیم آن را در عبارت with پایتون استفاده کنیم.

progress_2.py
from rich.progress import Progress
import time

with Progress(transient=True) as progress:
    task1 = progress.add_task("Downloading...", total=100)
    task2 = progress.add_task("Processing...", total=100)
    task3 = progress.add_task("Generating reports...", total=100)

    while not progress.finished:
        progress.update(task1, advance=0.5)
        progress.update(task2, advance=0.3)
        progress.update(task3, advance=0.1)

        time.sleep(0.005)

با متد add_task به progress bar تَسک اضافه می‌کنیم. نام عملیات را به عنوان اولین آرگومان و سپس تعداد مراحل این task را در قالب total keyword argument مشخص و به متد ارسال می‌کنیم. خروجی این متد task ID است که عددی integer است. سپس در یک حلقه بررسی می‌کنیم که آیا progress تمام شده است یا نه. درستی این شرط منوط به این است که تمام وظیفه‌ها یا task ها تمام شده باشند، در غیر این صورت حلقه یک بار دیگر تکرار می‌شود و task را آپدیت می‌کنیم. این کار به وسیله‌ی متد update انجام می‌شود. گفتیم که هر task دارای یک task ID است. متد update این taskID را برای شناسایی این که کدام task را باید آپدیت کند می‌گیرد. مقدار پیشرفت هر task توسط advance keyword argument تعیین می‌شود.

تصویر اجرای برنامه در لحظه‌ای مشخص به صورت زیر است:

"python rich progress"

transient که در کد بالا آن را در کانستراکتور Progress استفاده کرده‌ایم باعث می‌شود که پس از این که تمام task ها تمام شدند کل فضای اختصاص داده شده به progress bar ها از روی صفحه پاک شود. انگار که هیچ وقت progress bar ای آنجا نبوده است. این مورد را احتمالا هنگام استفاده از دستورهای man یا more یا less در خط فرمان سیستم‌های شبه یونیکس دیده‌اید که چگونه بعد از خاتمه دادن به برنامه تمام اطلاعات این برنامه‌ها از روی ترمینال حذف می‌شود.

کلاس Panel🔗

از کلاس Panel برای انداختن border دور متن یا هر renderable دیگری می‌توان استفاده کرد. تابع constructor این کلاس دو keyword argument مهم به نام‌های title و subtitle دارد که به ترتیب برای ست کردن «عنوان» در بالای پنل و «زیر عنوان» در پایین پنل استفاده می‌شوند. پنل‌ها به صورت پیش‌فرض از تمام عرض دسترس استفاده می‌کنند. اگر چنین رفتاری مطلوب شما نبود در تابع کانستراکتور expand=False را نیز وارد کنید.

panel_1.py
from rich.panel import Panel
from rich.console import Console, Group

panel1 = Panel(
    "[bold blue]expand=True[/]\n\n[red]This is default configuration of the Panel Class",
     title="Panel title",
     subtitle="Panel Subtitle",
     title_align='right',
     subtitle_align='left'
)
panel2 = Panel("[italic underline]expand=False[/]\n\ntitle and subtitle kwargs are not set", expand=False)

group = Group(panel1, panel2)

console = Console()
console.print(group)

در مثال فوق دو پنل تعریف کرده‌ایم. اولین پنل به صورت پیش‌فرض expand=True است و از تمام عرضی که در دسترسش است استفاده می‌کند. علاوه‌ بر این title و subtitle را برای آن ست می‌کنیم و صراحتا با kwarg های title_align و subtitle_align به آن می‌گوییم که title را در سمت راست و subtitle را در سمت چپ قرار دهد.

در پنل دوم در تابع کانستراکتور کلاس پنل expand را مساوی False قرار می‌دهیم و title و subtitle را هم سٍت نمی‌کنیم. همان طور که در تصویر زیر می‌بینید فضایی برای عنوان و زیر عنوان در نظر گرفته نمی‌شود و renderable دارای یک border یکپارچه و کامل است.

نکته حايز اهمیت دیگر این که می‌خواهیم دو آبجکتِ پنل را همزمان با هم در صفحه پرینت کنیم. به این منظور کلاس Group را import کردیم و از آن یک آبجکت ساختیم. renderable ها را به ترتیب مورد نظرخودمان به تابع کانستراکتور کلاس Group ارسال و نهایتا آبجکت حاصله از کلاس Group را در console پرینت کردیم.

خروجی کد بالا به صورت زیر است:

"rich panel python"

کلاس Layout🔗

کلاس Layout برای لایه بندی فضای ترمینال استفاده می‌شود. فرض کنید باید ترمینال را به دو بخش چپ و راست تقسیم کنیم و بخش چپ سه چهارم عرض ترمینال را بگیرد و بخش سمت راست یک چهارم باقی‌مانده را. بعد فرض کنید فضای سمت چپ را باید به دو قسمت بالا و پایین طوری تقسیم کنیم که هر دو بخش دارای ارتفاع برابر باشند. حالا ترمینال به سه بخش تقسیم شده است. الان می‌توانیم در هر بخش یک renderable قرار دهیم و نهایتا آبجکت layout اصلی را در console پرینت کنیم.

مثال بالا احتمالا یکی از بدترین انواع لایه بندی صفحه باشد ولی بیایید آن را پیاده‌سازی کنیم:

layout_1.py
from rich.console import Console
from rich.panel import Panel
from rich.layout import Layout
from rich.align import Align

main_layout = Layout()
main_layout.split_row(
    Layout(name="left", ratio=3),
    Layout(name="right", ratio=1)
)

main_layout["left"].split_column(
    Layout(name="left_top"),
    Layout(name="left_bottom")
)

left_top = Panel(Align("This is left-top panel", align='left', vertical='middle'))
left_bottom = Panel(Align("This is left-bottom panel", align='right', vertical='top'))
right = Panel(Align("This is right panel", align='center', vertical='bottom'))

main_layout['left_top'].update(left_top)
main_layout['left_bottom'].update(left_bottom)
main_layout['right'].update(right)

console = Console()
console.print(main_layout)

خروجی کد بالا به صورت زیر است:

"rich layout python"

با استفاده از کلاس Layout لایه‌ی اصلی را می‌سازیم و سپس با استفاده از آبجکت به دست آمده و متدهای split_row که برای تقسیم سطر و ایجاد ستون و متد split_column که برای تقسیم ستون و ایجاد سطر استفاده می‌شود می‌توانیم لایه‌های دیگر بسازیم. ورودی هر دو متد ابتدائا آبجکت‌های لایه‌های جدید و سپس kwargها هستند. ratio kwarg نسبت فضای لایه‌های فرزند به هم را مشخص می‌کند. مثلا در بالا گفته‌ایم که سه واحد فضا را به لایه‌ی left اختصاص بده و یک واحد فضا را به لایه right (به صورت ضمنی گفته‌ایم که چهار واحد فضا داریم.) لایه های جدید باید name keyword argument شان سِت شده باشد. این name بعدا به عنوان اندیس در آبجکت لایه‌ی اصلی استفاده می‌شود و می‌توانیم خیلی سریع به هر لایه دسترسی داشته باشیم:

layout_1.py
# ...
main_layout.split_row(
    Layout(name="left", ratio=3),
    Layout(name="right", ratio=1)
)

main_layout["left"].split_column(
    Layout(name="left_top"),
    Layout(name="left_bottom")
)
# ...

در کد بالا renderable ها (که در این جا همان متن‌های ما هستند) را به کلاس Align ارسال کرده‌ایم تا آن‌ها را آماده کنیم که در فضایی که نهایتا قرار می‌گیرند چپ-چین، راست-چین، وسط-چین و یا اینکه در بالا یا وسط یا پایین فضا قرار بگیرند. خروجی کلاس Align همچنان یک renderable است پس بدون دردسر می‌توانیم آن را به کلاس Panel ارسال کنیم تا دور آن یک border بیندازد:

layout_1.py
# ...
left_top = Panel(Align("This is left-top panel", align='left', vertical='middle'))
left_bottom = Panel(Align("This is left-bottom panel", align='right', vertical='top'))
right = Panel(Align("This is right panel", align='center', vertical='bottom'))
# ...

حالا سه پنل داریم که باید در داخل لایه‌ها قرار گیرند. در بالا گفتیم که هر لایه‌ی فرعی در هنگام ساخته شدن باید یک name keyword argument داشته باشد. با این name و استفاده از آن به عنوان اندیس آبجکت لایه‌ی اصلی می‌توانیم هر لایه‌ را پیدا کنیم. بعد از یافتن هر لایه، با فراخوانی متد update و ارسال پنل نظیر به عنوان آرگومان به آن، آن را آپدیت می‌کنیم و renderable جدید را درون آن لایه می‌اندازیم:

layout_1.py
# ...
main_layout['left_top'].update(left_top)
main_layout['left_bottom'].update(left_bottom)
main_layout['right'].update(right)
# ...

خُب. باقی کار ساده است. باید آبجکت لایه‌ی اصلی را در console پرینت کنیم؛ کاری که بارها آن را در مورد آبجکت کلاس‌های دیگر نیز انجام داده‌ایم.

کلاس Live🔗

با Live display بیگانه نیستید. آن را در Progress bar ها و status ها به کار برده‌ایم. با استفاده از مفهوم Live display و کلاس Live می‌توان یک renderable را بر روی ترمینال نمایش داد و سپس آن را در بازه‌های زمانی معین refresh کرد. به این صورت اطلاعات مداوما بر روی صفحه ترسیم می‌شود و کاربر بدون اینکه متوجه تغییرات وسیع انجام شده در پس‌زمینه شود نمایشی یکنواخت و بدون قطعی از اطلاعات در صفحه می‌بیند.

کلاس Live را باید با عبارت with و در قالب context manager استفاده کرد. وقتی کنترل از context manager خارج شود عملکرد Live display مرتبط با آن renderable خاص هم تمام می‌شود.

live_1.py
from rich.live import Live
from rich.table import Table
from rich.console import Console
from time import sleep

table = Table("Name", "Family", "City")

info = [
    {'Name': 'Mohsen', 'Family': 'Safari', 'City': 'Tehran'},
    {'Name': 'Sara',   'Family': 'Karimi', 'City': 'Isfahan'},
    {'Name': 'Abbas',  'Family': 'Rajabi', 'City': 'Tafresh'},
    {'Name': 'Taghi',  'Family': 'Afshar', 'City': 'Ghaemshahr'},
]

with Live(table):
    for i in info:
        table.add_row(i['Name'], i['Family'], i['City'])
        sleep(1)   

در کد بالا ابتدا آبجکتی از کلاس Table تعریف می‌کنیم و سپس آن را به کلاس Live ارسال می‌کنیم. Live این renderable را به صورت پی در پی بر روی صفحه ترسیم می‌کنیم. حالا اگر در میانه‌ی این رسم شدن‌ها اطلاعات را تغییر دهیم کاربر تغییرات را خیلی یکنواخت و روان بر روی صفحه می‌بیند.

ویدیوی زیر اجرای برنامه را نشان می‌دهد:

یک پروژه‌ی کوچک🔗

می‌خواهیم برنامه‌ای بنویسیم که از سایت randomuser.me اطلاعات چند کاربر فیک را بخواند و در سه progress bar روند پیشرفت دانلود و پیشرفت پردازش اطلاعات و پیشرفت درج اطلاعات در جدول را نمایش دهد. در این برنامه تقریبا از تمام کلاس‌های فوق استفاده می‌کنیم. تصویر زیر لحظه‌ای از زمان اجرای برنامه را نشان می‌دهد.

"تصویر برنامه‌ی ذیل در حال اجرا"

ویدیوی اجرای برنامه را نیز در همین سایت و از اینجا ببینید.

و اما کد برنامه:

dokaj_python_rich_program.py
from rich.console import Console
from rich.layout import Layout
from rich.table import Table
from rich.progress import Progress
from rich.panel import Panel
from rich.prompt import IntPrompt
from rich.text import Text
from rich.padding import Padding
from rich.align import Align
from rich.live import Live
from time import sleep
from urllib.request import urlopen
import sys
import json

def read_data():
    obj = urlopen("https://randomuser.me/api/?results=1")
    return json.loads(obj.read())["results"][0]

console = Console()
console.set_window_title("DOKAJ.IR")

number_of_records = IntPrompt.ask("How many records you want to read?", default=10)
if number_of_records <= 0:
    sys.exit(0)

layout = Layout()

layout.split_column(
    Layout(name="top"), 
    Layout(name="footer", size=3)
)

layout["top"].split_column(
    Layout(name="header", size=3),
    Layout(name="body")
)

layout["body"].split_row(
    Layout(name="left"),
    Layout(name="right")
)

layout["left"].split_column(
    Layout(name="progress", size=5),
    Layout(name="status"),
)

layout["header"].update(
    Panel(
        Align(
            "[bold]Playing with [link=https://rich.readthedocs.io/en/stable/index.html]\
Python's Rich module", align="center"
        )
    )
)
layout["footer"].update(
    Panel("Visit [black on white bold link=https://dokaj.ir]DOKAJ.IR[/] for more info.")
)

progress = Progress()
task_readding = progress.add_task("Read data...", total=number_of_records, start=False)
task_processing = progress.add_task("Processing data...", total=number_of_records, start=False)
task_updating = progress.add_task("Updating table...", total=number_of_records, start=False)

table = Table(
    "Name", "Family", "Country", "Age",
    expand=True, 
    header_style="bold", 
    title="Students list",
    title_style="Bold cyan",
    caption="This is my students list",
    caption_style="bold italic"
)

status = Text("", overflow="fold")

layout["progress"].update(Panel(progress, title="Progress"))
layout["status"].update(Panel(status, title="Status"))
layout["right"].update(Panel(table, title="Information"))

data = []

with Live(layout, refresh_per_second=4, vertical_overflow="visible") as live:
    progress.start_task(task_readding)
    while not progress.tasks[task_readding].finished:
        try:   
            result = read_data()
            data.append([
                result["name"]["first"],
                result["name"]["last"],
                result["location"]["country"],
                str(result["dob"]["age"])
            ])
            progress.update(task_readding, advance=1)
            status.append("New data fetched: ")\
                .append(f"{result['name']['first']} {result['name']['last']}\n",
                    style="bold italic cyan") 
        except Exception as e:
            status.append(f"{e}\n", style="red")
            sleep(1)

    progress.start_task(task_processing)    
    while not progress.tasks[task_processing].finished:
        progress.update(task_processing, advance=1)
        sleep(0.5)            

    data.reverse()
    progress.start_task(task_updating)             
    while not progress.tasks[task_updating].finished:
        info = data.pop()
        table.add_row(*info)
        progress.update(task_updating, advance=1)
        status.append("Table updated with: ") \
            .append(f"{info[0]} {info[1]}\n", style="bold italic blue")
        sleep(0.5)

کد فوق را به صورت زیر اجرا کنید:

SHELL
$ python dokaj_python_rich_program.py

پایان مطلب و نتیجه‌گیری🔗

در این راهنما نگاهی خیلی ساده به rich داشتیم ولی با همین سادگی کارهای بزرگی انجام دادیم که قبلا تصور انجام دادنش را هم نمی‌کردیم. اینجا نقطه‌ی پایان rich نیست. حجم بالایی از اطلاعات وجود دارد که در این راهنما پوشش داده نشده است. این سایت منبع بسیار خوبی برای تکمیل اطلاعات شماست. در python shell نیز می‌توانید در مورد کلاس‌ها و متدها بخوانید. مثلا در زیر در مورد کلاس Console کسب اطلاع می‌کنیم. چون حجم خروجی دستور بالاست فقط چند خط ابتدایی را قرار می‌دهم:

python shell
>>> from rich.console import Console
>>> help(Console)
Help on class Console in module rich.console:

class Console(builtins.object)
# ....

شاد باشید.


  1. برای مطالعه‌ در مورد context manager این صفحه را بخوانید. 

نوشته شده در: 1402-02-16 (1 سال 7 ماه 11 ساعت پیش)

من محسن هستم؛ برنامه‌نویس سابق PHP و Laravel و Zend Framework و پایتون و فلسک. تمرکزم بیشتر روی لاراول بود! الان از صفر مشغول مطالعات اقتصادی هستم.

برای ارتباط با من یا در همین سایت کامنت بگذارید و یا به dokaj.ir(at)gmail.com ایمیل بزنید.

در مورد این مطلب یادداشتی بنویسید.