pyproject.toml وملفات القفل وهيكلية مجلد src/ — بنية المشاريع الحديثة
من setup.py إلى pyproject.toml، وبناء يعتمد عليه مع uv.lock، وملف __init__.py وهيكلية src/ — وتحويل مجلد سكريبتات مسطح إلى حزمة حقيقية تدعم أوامر الطرفية.
كيف تريد أن تتعلم؟
المشكلة التي نقوم بحلها
بدأ العمل على مشروع فرز تذاكر الدعم الفني (ticket triage) على هيئة بضعة سكريبتات متجاورة في مجلد واحد: clean.py وscore.py وrun_triage.py. يقف كاتب الكود داخل المجلد، ويشغل الأمر python run_triage.py، فيعمل كل شيء على ما يرام. ثم خلال ثلاثة أشهر، وقعت المشكلات التالية:
- وضع أحد الزملاء سكريبت تقرير في مجلد فرعي باسم
scripts/، ففشل السكريبت فوراً بالخطأ:ModuleNotFoundError: No module named 'score'— على الرغم من أن ملفscore.pyيقبع في المجلد المجاور مباشرة. - أنشأ شخص ما ملفاً باسم
secrets.pyفي المجلد لحفظ مفتاح واجهة برمجية (API key)، وفجأة "تعطلت" وحدةsecretsالأصلية المدمجة في بايثون داخل سكريبت آخر! - انضم عضو جديد للفريق؛ فوجد ملف
setup.pyالقديم وملفrequirements.txtيدرجان الحزم بطريقتين مختلفتين تماماً، ولم يكن أحد في الفريق يدري أيهما أصح.
لا يوجد أي خطأ في منطق الكود نفسه؛ بل يكمن الخلل في هيكلية المشروع (Structure). في هذا الفصل، سنحول هذا المجلد إلى حزمة بايثون حقيقية واحترافية — تعتمد على ملف pyproject.toml، وملف قفل (lockfile)، وهيكلية مجلد src/ — وهو الشكل القياسي الذي تتبعه كل مستودعات بايثون الحديثة في بيئات العمل الحقيقية.
بنهاية هذا الفصل ستكون قادراً على
- شرح أسباب انتقال مجتمع بايثون من
setup.pyإلىpyproject.toml، وقراءة الأقسام الرئيسية في ملفpyproject.toml - توضيح ما يثبته ملف
uv.lockبدقة، واستخدام أوامرuv lockوuv syncوuv sync --lockedفي سياقاتها الصحيحة - إنشاء مشروع يعتمد هيكلية
src/باستخدامuv init --packageو--lib، وتوضيح وظيفة ملف__init__.py - التعرف على فخي الاستيراد الشائعين في المجلدات المسطحة، وإثبات كيف تكتشفهما هيكلية
src/مبكراً - تحويل مجلد سكريبتات مسطح إلى حزمة برمجية متكاملة لها نقطة انطلاق لسطر الأوامر (CLI Entry Point)
المتطلبات المسبقة: Git والفروع وطلبات السحب (Pull Requests)، بالإضافة إلى أوامر uv init وuv add وuv run من الفصل السابق له.
من setup.py إلى pyproject.toml
في مشاريع بايثون القديمة، ستشاهد غالباً ملفاً يشبه هذا:
from setuptools import find_packages, setup
setup(
name="ticket-triage",
version="0.1.0",
packages=find_packages(),
install_requires=["httpx"],
entry_points={"console_scripts": ["triage=ticket_triage.cli:main"]},
)وبجواره ملف requirements.txt، وغالباً ما يحتوي على قائمة مختلفة من الحزم! المشكلة الجوهرية في setup.py هي أنه برنامج تنفيذي: حتى لمجرد معرفة اسم المشروع أو اعتمادياته، يتعين عليك تشغيل الكود، وبما أنه كود بايثون، يمكن لأي شخص وضع أي شيء بداخله — قراءة ملفات، شروط معقدة، وحتى اتصالات عبر الشبكة. لذا عجزت الأدوات الآلية عن استنتاج أي معلومات عنه بيقين.
أما ملف pyproject.toml، فهو يقدم نفس هذه المعلومات على هيئة إعلان وصفي (declaration) — ملف نصي بسيط بصيغة TOML يُقرأ فقط ولا يُشغل برمجياً أبداً. أصبح هذا هو المعيار القياسي المعتمد في بايثون، وتدعمه أدوات uv، ومحررات الأكواد، وأدوات الاختبار، وفاحصات الجودة (linters). إليك ما ينتجه الأمر uv init --package ticket-triage:
[project]
name = "ticket-triage"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Nadia Karim", email = "[email protected]" }
]
requires-python = ">=3.12"
dependencies = []
[project.scripts]
ticket-triage = "ticket_triage:main"
[build-system]
requires = ["uv_build>=0.9.1,<0.10.0"]
build-backend = "uv_build"(تستخرج أداة uv اسم المؤلف وبريده تلقائياً من إعدادات Git لديك). يتألف الملف من ثلاثة أقسام رئيسية:
[project]— هوية المشروع واعتمادياته. يتولى أمرuv addإدارة قائمةdependenciesنيابة عنك.[project.scripts]— أوامر سطر الأوامر. السطرticket-triage = "ticket_triage:main"يعني: أنشئ أمراً في الطرفية باسمticket-triageيستدعي دالةmainداخل حزمةticket_triage.[build-system]— الأداة التي تحول المشروع إلى حزمة قابلة للتثبيت والتوزيع (محرك البناء build backend). تستخدمuvمحركها الخاصuv_build؛ وفي مشاريع أخرى قد تصادفhatchlingأوsetuptools— وكلها تؤدي الغرض نفسه.
لاحظ أن المشروع يسمى ticket-triage (بالشرطة العادية)، بينما اسم حزمة بايثون البرمجية هو ticket_triage (بالشرطة السفلية). فالشرطة العادية غير مسموح بها في أسماء وحدات بايثون — لأن import ticket-triage سيعاملها بايثون كعملية طرح حسابية — لذا تحولها uv تلقائياً نيابة عنك.
ملفات القفل (Lockfiles): الانتقال من "لقد عمل معي" إلى "يعمل دائماً بنفس النتيجة"
في ملف pyproject.toml، تُكتب الاعتماديات على هيئة نطاقات (ranges): فالقيمة "httpx>=0.28.1" تعني الإصدار 0.28.1 أو أي إصدار أحدث. وهذا أمر مقصود — فأنت تعلن عما يقبله مشروعك. لكن التثبيت اليوم والتثبيت بعد ثلاثة أشهر قد يجلبا إصدارين مختلفين تماماً، ناهيك عن الاعتماديات الفرعية الكثيرة التي تختبئ خلفهما.
يأتي ملف uv.lock لسد هذه الفجوة بدقة متناهية. فهو يسجل الإصدار الفعل الدقيق وبصمات التحقق (hashes) لكل حزمة — بما في ذلك جميع الحزم التابعة بعيدة المدى. وبوجود هذا الملف في المستودع، يثبت كل جهاز حاسوب، وكل خادم تكامل مستمر (CI)، وخادم الإنتاج النهائي، نفس النسخ تماماً دون أدنى اختلاف. هذا ما يُعرف برمجياً بـ "البناء القابل لإعادة الإنتاج (reproducible build)".
أربعة أوامر تؤدي أربع مهام مختلفة:
uv lock— يقرأpyproject.tomlويُنشئ ملف القفل أو يحدّثه، دون أن يثبت أي حزم في البيئة.uv sync— يجعل بيئة.venvمطابقة لملف القفل تماماً؛ ويحدث ملف القفل أولاً إذا دعت الحاجة.uv sync --locked— نفس الوظيفة السابقة، ولكنه يتوقف بخطأ بدلاً من التحديث إذا وجد ملف القفل قديماً وغير مطابق لـpyproject.toml.uv lock --check— لا يغير أي شيء، بل يفحص فقط هل يتطابق ملف القفل معpyproject.tomlأم لا.
يقوم أمرا uv add وuv remove بتحديث كلا الملفين معاً تلقائياً، لذا نادراً ما تحتاج لتشغيل هذه الأوامر منفردة أثناء عملك المعتاد. تبدأ المشكلات حينما يعدل شخص ما ملف pyproject.toml يدوياً. لنفترض أن شخصاً غيّر dependencies = [] إلى dependencies = ["httpx>=0.28"] يدوياً ولم يشغل أمر uv lock:
uv sync --locked
Resolved 8 packages in 57ms
The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.لهذا السبب تحديداً تشغل أنظمة التكامل المستمر (CI) دائماً الأمر uv sync --locked: لضمان عدم تسلل أي إصدار لم تتم مراجعته واعتماده بدقة.
أمرuv syncالعادي لن يظهر أي خطأ في هذه الحالة. إذا شُغل بدون خيار--lockedفي الموقف نفسه، ستقومuvبهدوء بإعادة حل الاعتماديات وتحديثuv.lockتلقائياً والاستمرار في العمل. هذا مريح جداً على جهازك الشخصي؛ لكنه في خوادم الاختبار يعني أن النظام يختبر شيئاً مختلفاً عما هو مسجل ومحفوظ في المستودع!
الحزم، ملف __init__.py، وهيكلية مجلد src/
الوحدة (module) هي ملف بايثون فردي .py. أما الحزمة (package) فهي مجلد يضم وحدات برمجية ويحتوي بداخله على ملف __init__.py. يؤدي ملف __init__.py وظيفتين: يخبر بايثون بأن هذا المجلد عبارة عن حزمة قابلة للاستيراد، وهو الملف الذي يُنفذ أولاً بمجرد كتابة import ticket_triage — ولذا يمكنك وضع واجهة الاستخدام العامة للمشروع (البوابة الرئيسية للحزمة) بداخله.
توفر أداة uv init ثلاثة خيارات مختلفة:
uv init— مشروع سكريبت بسيط: يضمmain.pyوpyproject.toml، دون نظام بناء. مناسب للمهام السريعة والتجارب في ملف أو ملفين.uv init --package— حزمة تطبيق متكاملة: تتبع هيكلية مجلدsrc/، مع نظام بناء وأمر تنفيذي داخل[project.scripts].uv init --lib— مكتبة برمجية مخصصة ليتم استيرادها من قبل الآخرين: تتبع هيكليةsrc/ونظام بناء، بدون أوامر تنفيذية، مع ملفpy.typedفارغ لإعلام فاحصات الأنواع (type checkers) بأن الحزمة تدعم تلميحات الأنواع.
الهيكلية الناتجة عن تشغيل uv init --package ticket-triage:
ticket-triage/
├── .git/
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
└── src/
└── ticket_triage/
└── __init__.pyتستقر الحزمة داخل مجلد باسم src/ بدلاً من وضعها مباشرة في جذر المشروع. لنرى سبب إضافة هذا المجلد الوسيط، دعنا نتعرف أولاً على ما يحدث من كوارث في المجلدات المسطحة.
فخا المجلد المسطح
الفخ الأول: أين يبحث بايثون عن الملفات المستوردة؟ عندما تشغل سكريبت بايثون، يبحث بايثون عن الوحدات المستوردة أولاً في المجلد الذي يوجد فيه السكريبت المشغَّل — وليس في المجلد الذي تقف فيه داخل الطرفية! في المجلد المسطح، تعمل عبارة from score import priority فقط لأن ملف score.py يتشارك نفس المجلد مع السكريبت. لننظر إلى سكريبت التقرير الذي وضعه الزميل في scripts/report.py:
from score import priority
print(priority("Database breach reported"))إذا وقفت في مجلد triage-scripts وشغلت python scripts/report.py:
Traceback (most recent call last):
File "scripts\report.py", line 1, in <module>
from score import priority
ModuleNotFoundError: No module named 'score'(في جهازك سيعرض تتبع الخطأ المسار كاملاً؛ تم تقصيره هنا). بحث بايثون داخل مجلد scripts/، ولا يوجد ملف اسمه score.py هناك! أن يعتمد نجاح الاستيراد على مكان وجود الملف هو أكثر قواعد التصميم هشاشة في أي مشروع برمجي.
الفخ الثاني: تصادم الأسماء (Name Collisions). بناءً على القاعدة ذاتها، يبحث بايثون في مجلد السكريبت قبل أن يبحث في وحداته ومكتباته القياسية المدمجة. إذا أنشأ شخص ما ملفاً باسم secrets.py في المجلد لحفظ مفتاح سري، وأراد ملف مجاور باسم new_id.py استخدام مكتبة secrets الأصلية التابعة لبايثون:
import secrets
print(len(secrets.token_hex(8)))Traceback (most recent call last):
File "new_id.py", line 3, in <module>
print(len(secrets.token_hex(8)))
^^^^^^^^^^^^^^^^^
AttributeError: module 'secrets' has no attribute 'token_hex'لم يظهر أي خطأ عند استدعاء import secrets — فقد عثر بايثون على ملف secrets.py المحلي واستورده! وقع الخطأ لاحقاً حين طلب الكود دالة غير موجودة في الملف المحلي. في مشاريع الذكاء الاصطناعي، يتكرر هذا الخطأ كثيراً حين يسمي المطورون ملفات تجاربهم بأسماء مثل openai.py أو anthropic.py أو httpx.py، مما يحجب المكتبة الرسمية التي يحاولون استخدامها!
كيف تكشف هيكلية مجلد src/ هذه الأخطاء مبكراً؟
يتغير أمران جوهريان عند الانتقال إلى هيكلية src/:
أولاً: يستقر كل شيء تحت اسم فريد محدد. لم تعد أسماء الوحدات عائمة مثل clean أو score أو secrets — بل أصبحت بأسماء واضحة: ticket_triage.clean وticket_triage.score. لا يمكن لأي ملف في المشروع أن يحجب مكتبة بايثون القياسية أو حزمة خارجية، وتوضح جمل الاستيراد بذاتها أي أجزاء الكود تخص مشروعك.
ثانياً: يستحيل استيراد الحزمة دون تثبيتها رسمياً. إذا حاولت استيراد الحزمة من جذر المشروع باستخدام أمر python التقليدي:
python -c "import ticket_triage"
Traceback (most recent call last):
File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'ticket_triage'هذا الفشل في الواقع هو ميزة مقصودة! لو كانت الحزمة في جذر المشروع مباشرة لنجح هذا الأمر — لأن بايثون سيجد المجلد أمامه فوراً — ولن تكتشف أبداً ما إذا كانت إعدادات pyproject.toml صحيحة أم خاطئة. أما داخل src/، فهناك طريقة واحدة فقط للوصول للحزمة: وهي تثبيتها. ويتكفل أمر uv run بتثبيت المشروع في بيئة .venv بوضع التعديل المباشر (editable mode) في كل مرة، مما يعني أن الحزمة المثبتة تشير مباشرة إلى مجلد src/ لديك دون الحاجة لإعادة التثبيت مع كل سطر كود تعدله:
uv run python -c "import ticket_triage; print(ticket_triage.priority('OUTAGE'))"
highوبالتالي، فإن أي خطأ في إعدادات بناء الحزمة سيظهر فوراً على جهازك مع أول تشغيل لـ uv run — وليس يوم النشر على الخوادم. على سبيل المثال، إذا كان مجلد الحزمة مكتوباً باسم خاطئ أو كان ملف __init__.py مفقوداً:
× Failed to build `ticket-triage @
│ file:///.../ticket-triage`
╰─▶ Expected a Python module at: src\ticket_triage\__init__.py(تم اختصار المسار). يشتق محرك البناء اسم الحزمة تلقائياً من اسم المشروع — من ticket-triage إلى src/ticket_triage/ — ويتوقف إذا لم يجد شيئاً هناك. وإذا أردت عمداً استخدام اسم مختلف، فيمكنك تحديده عبر خاصية module-name تحت قسم [tool.uv.build-backend] داخل pyproject.toml.
مثال عملي كامل
لنقم الآن بتحويل المجلد المسطح المذكور في بداية الفصل. بعد تنفيذ uv init --package ticket-triage --python 3.12 في مجلد مجاور ونقل الملفات القديمة إلى داخل الحزمة، تصبح الهيكلية كالتالي:
ticket-triage/
├── pyproject.toml
├── uv.lock
├── scripts/
│ └── report.py
└── src/
└── ticket_triage/
├── __init__.py
├── clean.py
├── cli.py
└── score.pyتعديل واحد في ملف pyproject.toml — تسمية مختصرة للأمر تشير إلى وحدة cli الجديدة:
[project.scripts]
triage = "ticket_triage.cli:main"ملف src/ticket_triage/clean.py — هو ملف clean.py القديم، ولكن بعد إضافة تلميحات الأنواع وسلسلة التوثيق (docstring):
import re
def clean_text(text: str) -> str:
"""Lowercase the text and collapse every run of whitespace to one space."""
return re.sub(r"\s+", " ", text).strip().lower()ملف src/ticket_triage/score.py — التغيير الجوهري الوحيد هو سطر الاستيراد: تحول من from clean import ... إلى from ticket_triage.clean import ...:
from ticket_triage.clean import clean_text
URGENT_WORDS = ("outage", "down", "breach")
def priority(text: str) -> str:
words = clean_text(text).split()
return "high" if any(word in URGENT_WORDS for word in words) else "normal"ملف src/ticket_triage/__init__.py — البوابة الرئيسية والواجهة العامة للحزمة:
from ticket_triage.score import priority
__all__ = ["priority"]ملف scripts/report.py — السكريبت الذي كان يتعطل سابقاً، أصبح يستورد الدوال الآن عبر اسم الحزمة بوضوح:
from ticket_triage import priority
print(priority("Database breach reported"))وأخيراً ملف src/ticket_triage/cli.py — نقطة الدخول التنفيذية لسطر الأوامر التي تحل محل run_triage.py القديم:
import sys
from ticket_triage.score import priority
DEMO_TICKETS = ["Login page is slow", "Payments DOWN for all users"]
def main() -> None:
tickets = sys.argv[1:] or DEMO_TICKETS
for ticket in tickets:
print(f"{priority(ticket):<6} {ticket}")
if __name__ == "__main__":
main()التشغيل من جذر المشروع:
uv run triage
uv run triage "Service down since morning" "Typo on the pricing page"
uv run scripts/report.py
uv lock --checknormal Login page is slow
high Payments DOWN for all users
high Service down since morning
normal Typo on the pricing page
high
Resolved 1 package in 2msفي المرة الأولى لتشغيل uv run triage، ستظهر بضعة أسطر إضافية قبل المخرجات — لإنشاء .venv وبناء الحزمة Building ticket-triage ثم Built ticket-triage — لأن uv تثبت الحزمة للمرة الأولى؛ ولن تظهر تلك الأسطر في المرات اللاحقة. السطر الأخير ناتج عن أمر uv lock --check، ورمز خروجه هو 0: دلالة على تطابق ملف القفل تماماً مع pyproject.toml.
القرارات الهندسية المتخذة في عملية التحويل هذه:
الاستيراد المطلق (from ticket_triage.clean import ...). الاستيراد النسبي متاح أيضاً داخل الحزمة — مثل from .clean import clean_text — لكن الاستيراد المطلق يُقرأ بنفس الوضوح من أي ملف ويسهل البحث عنه في المشروع بأكمله. أياً كان خيارك، حافظ على نمط واحد ثابت في المشروع.
تصدير priority فقط في ملف __init__.py. الكود الخارجي (scripts/report.py، الاختبارات المستقبلية، الخدمات الأخرى) لا يحتاج لمعرفة سوى from ticket_triage import priority. وإذا أعدنا تقسيم الكود بين score.py وclean.py مستقبلاً، فلن ينكسر الكود الخارجي أبداً. وقائمة __all__ تحدد هذه الواجهة العامة صراحة.
فصل ملف cli.py. عندما يكون المنطق البرمجي (score.py) مفصولاً عن معالجة المدخلات والمخرجات (cli.py)، يمكن استدعاء دالة priority من أي مكان — من اختبار آلي، أو من خادم API — دون الحاجة للتعامل مع sys.argv أو دوال الطباعة. وعندما تكون الوسائط sys.argv[1:] فارغة، تُستخدم التذاكر التجريبية الافتراضية، مما يضمن ظهور نتائج توضيحية حتى عند تشغيل الأمر دون مدخلات.
تنسيق المحاذاة f"{priority(ticket):<6}". يضمن الرمز :<6 محاذاة النص لليسار بعرض 6 أحرف، بحيث تبدأ نصوص التذاكر في نفس العمود المنظم سواء كانت الأولوية high أو normal.
هناك قصور طفيف يجدر التنبيه إليه: دالة clean_text تفصل الكلمات بناءً على المسافات البيضاء فقط، ولذا في جملة "The site is down."، تكون الكلمة الناتجة هي down. — شاملة النقطة في نهايتها — ولن تطابق قائمة URGENT_WORDS. سنتعلم طرقاً أكثر تطوراً لمعالجة النصوص لاحقاً في الدورة؛ وتركيز هذا الفصل هو على الهيكلية الصحيحة، وبفضلها فإن أي تحسين مستقبلي سيتم في ملف clean.py وحده دون أن تتأثر بقية أجزاء النظام.
عند حدوث مشاكل شائعة
ModuleNotFoundError: No module named 'clean' تم نقل الملفات إلى داخل الحزمة، لكن لا يزال أحد الملفات يحتوي على جملة الاستيراد القديمة from clean import clean_text. لا توجد وحدة مستقلة باسم clean في جذر المشروع؛ اكتب بدلاً منها from ticket_triage.clean import clean_text. بعد التحويل، ابحث في المشروع بالكامل عن from clean وfrom score.
ModuleNotFoundError: No module named 'ticket_triage' تم تشغيل الكود باستخدام أمر python العادي بدلاً من uv run، وبالتالي لم يتم تفعيل البيئة التي تحتوي على الحزمة المثبتة. شغّل الأمر باستخدام uv run .... هذا هو السلوك الطبيعي والمقصود لهيكلية src/، وليس خللاً.
Expected a Python module at: src\ticket_triage\__init__.py الحزمة ليست في المجلد الذي يتوقعه محرك البناء بناءً على اسم المشروع — إما لوجود خطأ إملائي في اسم المجلد (مثل src/triage/ أو src/ticket-triage/)، أو لغياب ملف __init__.py. صحح اسم المجلد، أو أضف __init__.py، أو حدد الاسم صراحة باستخدام module-name.
The lockfile at uv.lock needs to be updated, but --locked was provided. تم تعديل pyproject.toml يدوياً دون تشغيل uv lock، أو أن ملف القفل لم يتم حفظه في الالتزام. نفّذ أمر uv lock واحفظ كلا الملفين في التزام واحد. في المرات القادمة، عدل الاعتماديات باستخدام uv add وuv remove — فهما يضمنان تزامن الملفين معاً دائماً.
AttributeError: module 'secrets' has no attribute 'token_hex' يحتوي المشروع على ملف يحمل نفس اسم وحدة من وحدات مكتبة بايثون القياسية (مثل secrets.py، random.py، logging.py) مما حجب الوحدة الأصلية. أعد تسمية الملف — واحتفظ بالأسرار داخل ملف .env وليس داخل ملفات بايثون .py.
error: Failed to spawn: triag متبوعة بـ Caused by: program not found الاسم الذي مررته إلى uv run يختلف عن الاسم المعرف في قسم [project.scripts] — خطأ مطبعي في هذه الحالة. تحقق من الاسم في pyproject.toml؛ بعد تصحيحه ستقوم uv run بإعادة تثبيت المشروع تلقائياً وتفعيل الأمر الجديد.
الخطوة ٤ من ٦ — التوقع
تحقّق من فهمك
The chapter's clean_text is run on a messy ticket. What prints?
import re
def clean_text(text: str) -> str:
return re.sub(r"\s+", " ", text).strip().lower()
print(repr(clean_text(" Payments DOWN\tsince 9 ")))- A`' payments down since 9 '`
- B`'payments down since 9'`
- C`'payments down\tsince 9'`
- D`'Payments DOWN since 9'`
After moving from the flat folder to a src/ layout, uv run triage gives ModuleNotFoundError: No module named 'clean'. clean.py is right next to it, at src/ticket_triage/clean.py. What is the bug?
# src/ticket_triage/score.py
from clean import clean_text
URGENT_WORDS = ("outage", "down", "breach")
def priority(text: str) -> str:
words = clean_text(text).split()
return "high" if any(word in URGENT_WORDS for word in words) else "normal"- A`clean.py` must move back to the project root, because that is where Python looks
- BThe import is still flat-folder style; inside the package it must be `from ticket_triage.clean import clean_text`
- CWith an `__init__.py` in `src/ticket_triage/`, neighbouring files cannot be imported; it must be deleted
- D`uv run triage` only looks at `cli.py`; imports in `score.py` never run
In a PR, someone edited pyproject.toml by hand to add a package. CI's uv sync --locked fails: The lockfile at uv.lock needs to be updated. What is the right fix?
- ARemove `--locked` from CI so that CI updates the lockfile itself
- BAdd `uv.lock` to `.gitignore` so everyone builds their own lockfile
- CRun `uv lock` locally (or use `uv add` in the first place) and commit the updated `uv.lock` in the same PR
- DDelete `uv.lock`, since without a lockfile this error cannot appear
اللعب جزء من Pro
+30 XP بانتظارك
العب هذا الفصل مع Pro
القراءة مجانية، والفصل كله متاح. أول 5 فصول من كل دورة مجانية متاحة للعب مجانًا. ابتداءً من الفصل 6، الإجابة عن الأسئلة وكسب نقاط XP وتنمية سلسلتك جزء من Pro.
افتح اللعب مع Proدورك الآن
قم ببناء المجلد المسطح المذكور في بداية هذا الفصل بنفسك ثم حوله إلى حزمة احترافية. ضع أولاً ثلاثة ملفات في مجلد باسم triage-scripts/: ملف clean.py (دالة clean_text الموضحة أعلاه، بدون تلميحات أنواع)، وملف score.py (يبدأ بـ from clean import clean_text)، وملف run_triage.py الذي يطبع أولوية تذكرتين تجريبيتين. ثم:
- بجواره مباشرة، أنشئ مشروعاً جديداً بالأمر
uv init --package ticket-triage --python 3.12، وانقل الملفات إلى مجلدsrc/ticket_triage/، واجعل كل جمل الاستيراد تبدأ بـticket_triage. - حوّل
run_triage.pyإلى دالةmain()في ملفcli.pyكما هو موضح في هذا الفصل (بنفس التذكرتين التجريبيتين)، وأضف السطرtriage = "ticket_triage.cli:main"تحت قسم[project.scripts] - صدّر دالة
priorityفقط من ملف__init__.py، واستخدمها داخلscripts/report.pyعبرfrom ticket_triage import priority - شغّل
uv lock --checkللتأكد من تطابق ملف القفل، وسجل التزاماً يضمpyproject.tomlوuv.lockومجلديsrc/وscripts/في التزام واحد — دون تضمين.venv
من جذر المشروع، يجب أن ينتج عن تشغيل uv run triage متبوعاً بـ uv run scripts/report.py هذه المخرجات تحديداً:
normal Login page is slow
high Payments DOWN for all users
highبعد ذلك، تعمّد تعطيل المشروع لمرتين. الأولى: أعد جملة الاستيراد في score.py إلى from clean import clean_text وشاهد ما هو الخطأ الناتج وفي أي سطر يقع. والثانية: أنشئ ملفاً باسم secrets.py داخل src/ticket_triage/، وأضف import secrets وprint(len(secrets.token_hex(8))) إلى ملف cli.py، ثم شغّل uv run triage — هل وقع فخ المجلد المسطح هنا أيضاً، ولماذا لم يقع؟
الخطوة ٦ من ٦
التحدي — اختبار الفصل
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
العب مع Pro