# الـ Assert في Pytest مش اللي إنت فاكره! ## إيه هو AST؟ وإزاي Pytest بيهاكه عشان يديك رسائل خطأ أحسن؟ **تخيل معايا**: إنت بتكتب unit test (اختبار وحدة) لكود جديد، الاختبار فشل، وطلعلك رسالة **AssertionError** بتقولك "الاتنين مش متساوين" - **بس!** مش عارف إيه المشكلة بالضبط، ولا فين الاختلاف! **محبط، صح؟** طب تخيل لو الرسالة بتقولك **بالضبط** إيه العناصر المختلفة في القائمتين، وفين الاختلاف حصل! ده بالضبط اللي **Pytest** بيعمله - بس السؤال: **إزاي؟!** ## الـ Assert العادي في بايثون: بسيط بس مش مفيد أوي في بايثون (وفي لغات برمجة كتير)، فيه كلمة محجوزة اسمها **assert** - شغلتها بسيطة: **وظيفتها**: - ✅ لو الشرط **صح** (True) → ما تعملش حاجة - ❌ لو الشرط **غلط** (False) → ارمي **AssertionError** ![عبارة assert في بايثون](/static/img/pytest-assert-2.png) **يعني إيه بالعربي؟** لو بتختبر إن نتيجة دالة (function) معينة صحيحة، بتستخدم assert: ```python assert calculate_total([100, 200, 300]) == 600 # لازم يطلع 600 ``` **المشكلة**: لو الاختبار فشل، الرسالة مش بتقولك **إيه** المشكلة بالضبط! ### مثال يوضح المشكلة **تخيل**: عندك قائمتين (lists) عايز تتأكد إنهم متطابقين: ![رسائل خطأ مش وصفية من assert العادي](/static/img/pytest-assert-3.png) **الرسالة بتقول**: "AssertionError" - **وبس!** - مش عارف **فين** الاختلاف - مش عارف **إيه** القيم المختلفة - لازم **تدور بنفسك** في الكود **طب شوف بقى لما نستخدم Pytest** في نفس الكود: ![رسائل خطأ مفصلة من Pytest](/static/img/pytest-assert-4.png) **ياه!** دي رسالة **مفيدة فعلاً**: - بتقولك **فين** الاختلاف بالضبط - بتوريك **القيم** القديمة والجديدة - **توفر عليك وقت** التدوير **السؤال الكبير**: إزاي Pytest بيعمل كده؟! هو قدر **يغير** سلوك كلمة محجوزة في بايثون؟! ## هل ممكن نعيد تعريف كلمة محجوزة في بايثون؟ **السؤال ده** ودّاني في **رحلة** عشان أفهم إزاي Pytest بيشتغل! **الفكرة**: الطريقة الوحيدة إن Pytest يديك رسائل مفصلة كده، هي إنه **يغير** طريقة عمل الـ **assert** نفسها! **المشكلة**: بعد بحث سريع، هتلاقي إن **مفيش طريقة** تعيد تعريف الكلمات المحجوزة (keywords) في بايثون! ### مكتبات تانية عملت إيه؟ **unittest** (مكتبة الاختبارات القديمة في بايثون): - عارفة إنها **مش تقدر تغير** الـ assert - **عملت methods جديدة** بدل ما تستخدم assert - مثلاً: **assertEqual**، **assertTrue**، إلخ ![Unittest بتعمل methods خاصة بدل assert](/static/img/pytest-assert-5.png) **يعني**: unittest استسلمت وعملت حل بديل! **طب Pytest؟** Pytest لسه **بيستخدم assert** العادي - إزاي بقى يهرب من القيد ده؟ **الإجابة**: لازم نفهم حاجة اسمها **Abstract Syntax Tree** (AST)! ## الشجرة النحوية المجردة (AST): إزاي بايثون بتفهم الكود؟ **تخيل معايا** لما بايثون بتشغل الكود بتاعك: ### الخطوة الأولى: تقسيم لـ Tokens (رموز) بايثون بتقسم الكود لـ **رموز صغيرة**: - كلمات محجوزة: `if`، `for`، `while` - أسماء متغيرات: `x`، `total`، `user_name` - أرقام: `3.14`، `42`، `100` - عمليات: `==`، `+`، `*` ### الخطوة التانية: تنظيم في شجرة (AST) الرموز دي بتتنظم في **شجرة** بتوضح: - **مين يتنفذ الأول؟** - **مين يتبع مين؟** - **إيه أولوية العمليات؟** **مثال**: شوف المعادلتين دول: ![مثال AST مع المعادلات](/static/img/pytest-assert-6.png) ```python 1 + 2 * 3 # النتيجة: 7 (1 + 2) * 3 # النتيجة: 9 ``` **ليه النتايج مختلفة؟** لأن الـ **AST** مختلف! - في الأولى: الضرب **الأول**، بعدين الجمع - في التانية: الجمع **الأول** (بسبب الأقواس)، بعدين الضرب **يعني**: الـ AST هي اللي بتحدد **ترتيب تنفيذ** الكود حسب **قواعد اللغة**! ### الخطوة التالتة: تحويل لـ Bytecode الشجرة دي بتتحول لـ **bytecode** (كود بايت) - ده اللي بايثون بتفهمه فعلاً وبتشغله. ## الحيلة السحرية: بايثون بتسمحلك تعدل الـ AST! **ده المفتاح**: بايثون بتديك **hooks** (نقاط دخول) في عملية **import** (استيراد الملفات)! **معنى كده**: - لما بايثون بتستورد ملف - تقدر **تتدخل** في العملية - تقدر **تغير الكود** قبل ما يتشغل - تقدر **تعدل الـ AST** على الطاير! **ده بالضبط** اللي Pytest بيعمله! شوف جزء من **الكود الأصلي** لـ Pytest: ![كود Pytest الأصلي](/static/img/pytest-assert-7.png) **باختصار**: Pytest بيعمل كده: 1. لما **يستورد** ملف الاختبار بتاعك 2. بيقرا الـ **AST** بتاعه 3. بيلاقي كل **assert** في الكود 4. بيستبدلها بـ **if statement** مع كود بيطبع تفاصيل الخطأ! **والجزء الجميل**: بايثون **جاهزة بكده!** فيه module مدمج اسمه **ast** بيساعدك تعمل كل ده! ## إزاي نستخدم وحدة AST في بايثون؟ خلينا نشوف **بالكود** إزاي نلعب بالـ AST: ### خطوة 1: تحويل كود لـ AST ```python import ast code = "x = 1 + 2 * 3" tree = ast.parse(code) # حول الكود لشجرة ``` ### خطوة 2: عرض الشجرة ```python print(ast.dump(tree)) # اطبع الشجرة بشكل قابل للقراءة ``` ![عرض AST الكود](/static/img/pytest-assert-9.png) ### خطوة 3: تعديل الشجرة بايثون بتوفرلك **ast.NodeTransformer** - class جاهز يساعدك تعدل الشجرة بسهولة! **يعني**: مش محتاج تكتب كود معقد عشان تعبر الشجرة - **بايثون مجهزاك!** ## وقت التطبيق: نعمل Pytest مصغر! **تخيل معايا**: عايزين نعمل حاجة شبه Pytest - بس بدل ما نعدل assert، هنعمل حاجة **أبسط وأوضح**! **الفكرة**: هنعمل كود **يقرب الأرقام تلقائياً** في الاختبارات! ### الملف الأول: my_test.py عندنا ملف فيه اختبار بسيط: ![ملف my_test.py](/static/img/pytest-assert-10.png) ```python def test_equal(): assert 3.14159 == 3.14 # دي هتفشل - الرقمين مش متطابقين! ``` **المشكلة**: الاختبار ده **هيفشل** لأن `3.14159` مش بالضبط `3.14`! ### الملف التاني: tester.py (بدون تعديل) لو استوردنا my_test.py عادي: ![ملف tester.py بدون تعديل](/static/img/pytest-assert-11.png) ```python from my_test import test_equal test_equal() # AssertionError - فشل! ``` **النتيجة**: ❌ **AssertionError** - فشل! ### الحل: نعدل الـ AST قبل الاستيراد! **الخطة**: 1. نعترض **عملية الاستيراد** (import) 2. نقرا محتوى `my_test.py` 3. نعمل **AST** للكود 4. نستبدل كل الأرقام العشرية بـ **round()** - يقرب الرقم 5. نحفظ الـ AST المعدل ونشغله! **الكود**: دالة بتعدل الـ AST: ![دالة auto_round في tester.py](/static/img/pytest-assert-12.png) **شرح الكود**: 1. **ast.parse**: حول الكود لـ AST 2. **NodeTransformer**: عدّل كل رقم عشري → حطه جوا `round()` 3. **ast.unparse**: حول الـ AST رجوع لكود عادي **الكود**: hook للاستيراد: ![Hook الاستيراد في tester.py](/static/img/pytest-assert-13.png) **شرح الـ Hook**: - لما بايثون تستورد `my_test.py` - الـ hook بيشتغل **قبل** ما الملف يتحمل - بيستخدم `auto_round` عشان يعدل الكود - بعدين بيحمل الكود المعدل! **النتيجة النهائية**: ```python # الكود الأصلي كان: assert 3.14159 == 3.14 # بعد التعديل بقى: assert round(3.14159) == round(3.14) # النتيجة: ✅ نجح! - لأن الاتنين بيطلعوا 3 ``` ## الخلاصة: فهمنا إيه؟ **رحلة اليوم** علمتنا: ### 1. المشكلة الأصلية - **assert** العادي في بايثون بيديك رسائل **مش مفيدة** - مش بيقولك **إيه** المشكلة بالضبط - **Pytest** بيحل المشكلة دي - إزاي؟ ### 2. الحل السحري: AST - **Abstract Syntax Tree** (الشجرة النحوية المجردة) - بايثون بتحول كل كود لـ **شجرة** - الشجرة دي بتوضح **ترتيب التنفيذ** وعلاقة الأجزاء ببعضها ### 3. القوة الخفية في بايثون - بايثون بتسمحلك **تعدل الـ AST** قبل التنفيذ! - تقدر **تعترض** عملية الاستيراد (import) - تقدر **تغير الكود** على الطاير! ### 4. Pytest استغل القوة دي - بيعترض **استيراد ملفات الاختبارات** - بيعدل الـ **assert** statements - بيستبدلها بـ **if statements** بترسائل مفصلة! ### 5. التطبيق العملي - عملنا مثال بسيط: **auto-round** - فهمنا إزاي نستخدم **ast.parse** - فهمنا إزاي نعمل **import hooks** **الخلاصة**: Pytest مش **سحر** - هو **استغلال ذكي** لميزات بايثون المدمجة! تقدر تشوف الكود الكامل [هنا](https://github.com/gr33ndata/notebooks/tree/master/notebooks/PytestAST) لو عايز تجربه بنفسك! --- طارق عمرو، 13 يناير 2023