اختصار مهام بيئات التطوير البرمجية عبر مكتبة invoke
بواسطة سليمان القسيمي
بتاريخ 07 / 02 / 2025
الأعزاء الكرام, أصدقاء البرمجة في كل مكان, تحية طيبة وبعد:
أطل عليكم اليوم للحديث عن أحد مكتبات بايثون التي قد لا يكون لها وقعها الكافي بالنسبة للمستخدم بقدر ما تمثل أداة لتسهيل العمل عليك أنت أيها المبرمج في التفاعل مع مشروعك البرمجي.
تصور عزيزي أنك تقوم بالعمل على أحد المشاريع البرمجية بلغة بايثون, وليكن مشروعك هذا برنامجًا يعمل على أجهزة الحاسوب العاملة بنظام Windows.
هذا المشروع قد اكتسب شيئًا كبيرًا من التوسع بحيث أصبحت إدارة عملياته بشكل يدوي مسألة لا تطيق تنفيذها بشكل مستمر.
ولكي نكون جميعًا في الصورة؛ دعونا نأخذ عملية تحويل البرنامج إلى exe والذي قد لا يكون في حالة برنامجك الواسع بتلك السهولة المتمثلة في تنفيذ أمر pyinstaller واحد كافية لتجميع البرنامج بالكامل بحكم أن البرنامج يحوي على ملفات صوتية خارجية يجب نسخها بشكل يدوي بعد التحويل, أو ملفات خاصة بالترجمة أو دليل الاستخدام أو ما شابه ذلك من مهام مصاحبة لعملية التحويل.
ماذا إن أمكن اختصار جميع تلك الأوامر بسطر واحد يقوم هو بتولي تلك العمليات بشكل تلقائي؟ هذا بالضبط والتمام ما تقدمه لنا مكتبة invoke التي نحن بصدد الحديث عنها اليوم.
مكتبة invoke هي أداة تسمح لك بتعريف عدد من المهام لتقوم لاحقًا بتشغيلها من خلال موجه الأوامر وذلك في سياق مسار البرنامج الخاص بك
يتم كتابة تلك المهام في ملف بايثون يسمى tasks.py وذلك على شكل دوال مزخرفة تمثل المهام المطلوبة.
في المشروع الواحد؛ تحتاج إلى إنشاء ملف tasks.py واحد هو الذي يضم جميع المهام الروتينية للمشروع, ونقوم عادة بوضعه في المجلد الرئيسي للمشروع لكي نتمكن من تنفيذ المهام من أية مجلدات فرعية داخل المشروع.
دعونا نأخذ الآن مثالًا بسيطًا في آلية عمل المكتبة
قم أولًا بتثبيت المكتبة عن طريق جملة pip كما يلي
pip install invoke
سنحتاج في مثالنا إلى مكتبة pyinstaller قم بتثبيتها هكذا
pip install pyinstaller
قم الآن بإنشاء مجلد ولنسميه مثلًا demo, ضع في هذا المجلد ملف بايثون وقم بتسميته demo.py
إن مجلد demo والملف demo.py ما هو إلى مشروع بسيط لمجرد التجربة, وبإمكانك تجاهل هذه الخطوة إذا كان لديك مشروعًا آخر لكي تجرب عليه
طيب, نريد بعض من الملحقات لكي نضعها بجانب ملف demo.py, قم بإنشاء مجلد للغات ولنسميه مثلًا "languages" ومجلد آخر لدليل الاستخدام ولنسميه "documentation"
من جديد, هذه كلها مجلدات للتجربة ويمكنك استخدام أي مشروع جاهز لديك.
في ملف demo.py دعونا نكتب برنامج بسيط للتجربة
# demo.py
import random
while True:
seecrit = random.randint(1, 10)
for i in range(5):
guess = int(input("خمن رقمًا من 1 إلى 10:"))
if guess > seecrit:
print("خطأ, الرقم أصغر من ذلك")
elif guess < seecrit:
print("الرقم أكبر من ذلك")
else:
print("جواب صحيح")
break
else:
print(f"استنفذت جميع الفرص الخمسة. الجواب الصحيح هو {seecrit}")
print("هل تريد اللعب من جديد: ")
print("1. نعم\n2. لا")
option = input()
if option != "1":
break
# end
الآن, لو أردنا الاستفادة من مكتبة invoke في تجميع هذا البرنامج على افتراض أنه يحتاج إلى بعض الملحقات ولتكن مجلدات اللغة والدليل الإرشادي كما افترضنا سابقًا, فلا يكتفى فقط تنفيذ الأمر pyinstaller demo.py, بل إن عليك نسخ مجلدات اللغة والدليل بشكل يدوي إلى مجلد البرنامج المحول إلى exe وهذه مسألة إن بدت لنا سهلة في هذا المشروع المبسط فهي من أكثر الأمور إرهاقًا في المشاريع الكبيرة؛ ناهيك عن النسيان الذي قد يحدث.
الحل هنا هو أن نقوم بإنشاء ملف يحتوي على أهم المهام اللازمة للمشروع, قم بإضافة ملف باسم tasks.py إلى المجلد الرئيسي لمشروعك.
في هذا الملف؛ سنقوم بإنشاء دوال بايثون, وهذه الدوال تمثل المهام التي يمكن تشغيلها من موجه الأوامر.
سأقوم في هذه الحالة بإنشاء دالة لبناء المشروع, ودالة أخرى لنسخ ملحقاته, ثم سنتعرف على طريقة ربطهما أو تشغيل كل مهمة على حدة
# tasks.py
import os
os.chdir(os.path.dirname(_file_))
كتبنا السطرين السابقين لضمان قراءة مسار مجلد المشروع من بدايته, أي من المجلد الرئيسي, حتى لو قمنا بتشغيل المهام من أحد مجلدات المشروع الفرعية
# tasks.py
from invoke import task
import shutil
قمنا باستدعاء دالة task من مكتبة invoke وهي عبارة عن دالة نضعها قبيل تعريف أية دالة أخرى لتحويلها من دالة بايثون إلى مهمة يمكن تشغيلها من موجه الأوامر
أما مكتبة shutil فسنحتاج إليها لاحقًا لنسخ المجلدات والملفات
# tasks.py
@task
def copy_dependencies(c):
folders = ["languages", "documentation"]
print("copying dependencies")
for folder in folders:
print(f"copying {folder}...")
shutil.copytree(folder, os.path.join("dist", "demo", folder))
print("done")
دعونا ننظر قليلًا إلى المهمة copy_dependencies
كما تلاحظون أن المهمة ليست إلا مجرد دالة بايثون اعتيادية مع فارقين اثنين
الفرق الأول وجود زخرفة @task قبل جملة تعريف الدالة
المزخرفات في بايثون هو موضوع سبق أن تحدثنا عنه في وقت سابق, وباختصار المزخرفات decorators هي عبارة عن دوال بايثون تقوم بتغيير سلوك دوال بايثون أخرى, وفي حالتنا هذه, عليك أن تفهم فقط أن المزخرفة task ستقوم بتحويل دالة بايثون الاعتيادية إلى مهمة invoke يمكن تنفيذها من موجه الأوامر
أما الفرق الثاني فهي أن هذه الدالة قد أوجبت علينا إنشاء معاملًا افتراضيًا لها, وهذ المعامل هو عبارة عن كائن يتم تمريره إلى جميع مهام invoke أثناء تنفيذها بحيث يمكن من خلال هذا المعامل تنفيذ أوامر cmd مباشرة من الدالة كما سنرى في المهمة الثانية
في مهمة copy dependencies قمنا بتعريف قائمة list وضعنا فيها أسماء المجلدات التي نرغب بنسخها إلى مجلد dist/demo وهو مجلد البرنامج الذي سينتج تلقائيًا بعد تحويله إلى exe عن طريق pyinstaller.
بعدها قمنا بالدوران على تلك المجلدات واحدًا تلو الآخر لنقوم بنسخها جميعًا إلى المسار المحدد dist/demo بحيث لا نحتاج إلى كتابة أمر النسخ لكل مجلد على حدة
لاحظ معي انني قد قمت بوضع جمل print كعملية تنظيمية استرشاديةليس إلا.
# tasks.py
@task
def build(c):
print("running py installer")
c.run("pyinstaller demo.py")
المهمة الثانية قمنا بتسميتها build وهذه المهمة هي التي ستقوم بتشغيل مكتبة pyinstaller على الملف demo.py لتحويله إلى exe
لاحظ هنا أن المعامل c الذي سبق أن شرحناه سابقًا قد أصبح ذو فائدة عملية, حيث قمنا باستخدامه لتشغيل أمر cmd الخاص بمكتبة pyinstaller مباشرة من دالة بايثون.
دعونا الآن نقوم بتجميع الملف كاملًا لتجربته
# tasks.py
import os
os.chdir(os.path.dirname(_file_))
from invoke import task
import shutil
@task
def copy_dependencies(c):
folders = ["languages", "documentation"]
print("copying dependencies")
for folder in folders:
print(f"copying {folder}...")
shutil.copytree(folder, os.path.join("dist", "demo", folder))
print("done")
@task
def build(c):
print("running py installer")
c.run("pyinstaller demo.py")
الآن, قم بفتح موجه الأوامر cmd في مسار مجلد المشروع demo وجرب كتابة الأمر التالي
invoke build
لاحظ أنه قد تم تشغيل الدالة build وتم تحويل المشروع إلى exe
الآن دعونا نقوم بتشغيل المهمة copy_dependencies وانتبه هنا إلى أنه بحكم أن اسم الدالة copy_dependencies تحتوي على علامة الشرطة السفلية "_" فإن هذه العلامة سيتم تحويلها إلى شرطة قياسية "-" عند تنفيذ الأمر من موجه الأوامر هكذا
invoke copy-dependencies
اتجه إلى dist/demo لتجد أن كلًا من المجلد languages, و documentation قد تم نسخهما إلى المسار المحدد.
الآن تتبقى لنا خطوة بسيطة وهي أن نجعل المهمة build تقوم تلقائيًا باستدعاء المهمة copy_dependencies عند الانتهاء, وهذه العملية ستختصر علينا مسألة كتابة الأمر invoke copy-dependencies بعد عملية البناء
# tasks.py
@task(post=[copy_dependencies])
def build(c):
print("running py installer")
c.run("pyinstaller demo.py")
تلاحظ معي هنا أن متن الدالة build بقي على حاله ولم يطرأ عليه أي تغيير
الاختلاف هنا أنه قمنا بتوسيع سلوك المزخرفة task وذلك بتحديد قيمة للمعامل post
إن المعامل post هو عبارة عن قائمة يمكن استخدامها لوضع مجموعة من المهام الأخرى ليتم تنفيذها بعد الانتهاء من تنفيذ المهمة الأساسية
وفي حالتنا هذه قمنا بوضع عنصرًا واحدًا في تلك القائمة والذي هو بطبيعة الحال المهمة copy_dependencies بحيث يتم تشغيلها مباشرة بعد تنفيذ المهمة build
ومن باب المعرفة بالشيء, يمكن تعيين قائمة بالمهام التي يجب تشغيلها قبل المهمة الأصلية وذلك بتعيين المعامل pre هكذا
@task(pre=[task1], post=[task2])
فلو كانت هناك مهمة باسم task1 سيتم تشغيلها قبل الدالة المزخرفة, وأما المهمة task2 سيتم تشغيلها بعد الدالة المزخرفة
لنجرب الآن كتابة الأمر invoke build ونرى ما سيحدث
running py installer
142 INFO: PyInstaller: 6.11.1, contrib hooks: 2025.0
142 INFO: Python: 3.9.9
151 INFO: Platform: Windows-10-10.0.19045-SP0
151 INFO: Python environment: C:\Users\suleiman\AppData\Local\Programs\Python\Python39
152 INFO: wrote C:\Users\suleiman\tests\demo\demo.spec
155 INFO: Module search paths (PYTHONPATH):
['C:\\Users\\suleiman\\AppData\\Local\\Programs\\Python\\Python39\\Scripts\\pyinstaller.exe',
'C:\\Users\\suleiman\\AppData\\Local\\Programs\\Python\\Python39\\python39.zip',
'C:\\Users\\suleiman\\AppData\\Local\\Programs\\Python\\Python39\\DLLs',
'C:\\Users\\suleiman\\AppData\\Local\\Programs\\Python\\Python39\\lib',
'C:\\Users\\suleiman\\AppData\\Local\\Programs\\Python\\Python39',
'C:\\Users\\suleiman\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages',
'C:\\Users\\suleiman\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\win32',
'C:\\Users\\suleiman\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\win32\\lib',
'C:\\Users\\suleiman\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\Pythonwin',
'C:\\Users\\suleiman\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\setuptools\\_vendor',
'C:\\Users\\suleiman\\tests\\demo']
615 INFO: checking Analysis
620 INFO: checking PYZ
628 INFO: checking PKG
629 INFO: Bootloader C:\Users\suleiman\AppData\Local\Programs\Python\Python39\lib\site-packages\PyInstaller\bootloader\Windows-64bit-intel\run.exe
629 INFO: checking EXE
629 INFO: checking COLLECT
629 INFO: Building COLLECT COLLECT-00.toc
649 INFO: Building COLLECT COLLECT-00.toc completed successfully.
copying dependencies
copying languages...
copying documentation...
done
هذا كل ما في الأمر
أرفق لكم محتوى الملف tasks.py كاملًا, وقبلها أشكر لكم قراءتكم وأرجو أن يكون في هذه الأسطر ما قد أضاف إلى حصيلتكم البرمجية ولو بشكل اليسير.
# tasks.py
import os
os.chdir(os.path.dirname(_file_))
from invoke import task
import shutil
@task
def copy_dependencies(c):
folders = ["languages", "documentation"]
print("copying dependencies")
for folder in folders:
print(f"copying {folder}...")
shutil.copytree(folder, os.path.join("dist", "demo", folder))
print("done")
@task(post=[copy_dependencies])
def build(c):
print("running py installer")
c.run("pyinstaller demo.py")