الانتقال إلى Ruff: فحص وتنسيق بايثون بأداة واحدة

بقلم فريق تقني · (آخر تحديث: )· أدوات المطورين
الانتقال إلى Ruff: فحص وتنسيق بايثون بأداة واحدة

أول ruff check . على شجرة عمرها سنتان يُرجع عادةً آلاف السطور دفعة واحدة، وردّ الفعل المتوقّع — تعطيل نصف القواعد أو التراجع عن الفكرة — هو ما يُفشل أغلب محاولات الهجرة، لا نقصٌ في الأداة. وقد صارت المسألة أحدّ منذ يوليو 2026، حين غيّر Ruff مجموعته الافتراضية تغييراً جذرياً، فباتت أغلب الشروح المنشورة قبل ذلك التاريخ تصف إعداداً لم يعد قائماً. ما يلي ترتيب الهجرة كما يعمل فعلاً: أي مفتاح يوقع الأكثرية في الفخ، وأي إصلاح آلي يغيّر سلوك برنامجك بصمت، وكيف تُقسّم العمل على دفعات تمرّ في المراجعة.

ما الذي يخرج من مشروعك وما الذي يبقى

Ruff يحلّ محل flake8 وعشرات إضافاته، إلى جانب black وisort وpydocstyle وpyupgrade وautoflake، بقواعد أُعيد تنفيذها أصلاً داخله. وهي مرتّبة في عائلات لكل منها بادئة تفعّلها أو تطفئها بحرف أو حرفين: F لـ Pyflakes، وE/W لـ pycodestyle، وI لـ isort، وB لـ flake8-bugbear، وC4 لـ flake8-comprehensions، وSIM لـ flake8-simplify، وUP لـ pyupgrade، وN لـ pep8-naming، وPL لـ pylint، وRUF للقواعد الأصلية.

وقبل أن تكتب سطراً واحداً، عقبتان تخصّان المهاجرين. الأولى أن أداة التحويل الرسمية flake8-to-ruff لم تعد مدعومة، فنقل إعداداتك يدويٌّ بالكامل. والثانية أن Ruff نقل إعدادات المدقّق من [tool.ruff] إلى [tool.ruff.lint] منذ الإصدار 0.2.0، فأي مقتطف من دليل أقدم يضع select تحت [tool.ruff] لن يُقرأ كما تتوقّع. وإن كنت تجدّد عدّتك كاملة لا الفحص وحده، فـRuff قطعة من صورة أوسع فكّكناها في دليل أدوات بايثون الحديثة.

select يستبدل الافتراضي ولا يضيف إليه

الإعداد كله يعيش تحت [tool.ruff.lint]، والمجموعة الافتراضية نفسها تحرّكت تحرّكاً كبيراً. ظلّت سنوات مجموعة محافظة من 59 قاعدة هي E4 وE7 وE9 مع F، ثم وسّعها إصدار Ruff 0.16.0 في 23 يوليو 2026 إلى 413 قاعدة مفعّلة افتراضياً، وأزال في الوقت نفسه 18 قاعدة كانت ضمنها. ولمن يريد السلوك القديم أن يعيده صراحة بـselect = ["E4", "E7", "E9", "F"]. وبقية المفاتيح افتراضاتها ثابتة: extend-select وignore وunfixable فوارغ، وfixable هو ["ALL"]، وper-file-ignores هو {}.

ثم تأتي النقطة التي تُسقط الأكثرية: select يستبدل المجموعة الافتراضية بالكامل ولا يضيف إليها. من يكتب select = ["B"] ظنّاً أنه أضاف bugbear فوق ما هو مفعّل، يكون قد أطفأ F وE دون أي رسالة تنبّهه — فقد اكتشاف المتغيّرات غير المعرّفة والاستيرادات الميتة مقابل قواعد أسلوبية. الإضافة بـextend-select وحده. وfixable/unfixable تحدّدان أي القواعد يُسمح لـ--fix أن يمسّها، وهما مخرجك حين تريد فحصاً واسعاً وإصلاحاً آلياً ضيّقاً.

الفخ نفسه منصوب مرة ثانية في مكان لا يتوقّعه أحد: exclude يدوس قائمة الاستبعاد الافتراضية كاملةً، وفيها .venv وnode_modules وdist. فمن يكتب exclude = ["migrations"] يكون قد أعاد فتح البيئة الافتراضية للفحص، ويرى آلاف المخالفات في شفرة ليست شفرته. والإضافة هنا بـextend-exclude. القاعدة الذهنية واحدة: ما لا يحمل بادئة extend يستبدل، وما يحملها يضيف.

[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I"]
extend-select = ["B", "UP"]
ignore = ["E501"]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"tests/*.py" = ["D"]

[tool.ruff.lint.isort]
known-first-party = ["myapp"]

وإن كان ملف الإعداد نفسه غير مألوف لديك، فمرجعه في شرح pyproject.toml.

أمر يفحص وأمر ينسّق: الخلط بينهما أشهر عثرات اليوم الأول

ruff check يفحص، وruff format ينسّق بديلاً عن black، ولا أمر ثالث يجمعهما حتى اليوم. الفحص لا ينسّق شيئاً، والمنسّق لا يفحص شيئاً.

وثلاثة أعلام تختصر يومك الأول. --statistics يعطي عدّاداً لكل قاعدة لها مخالفة واحدة على الأقل، وهو تقريرك قبل تغيير أي سطر. و--diff لا يكتب أي ملف، بل يطبع على الخرج القياسي ما كان سيغيّره، فتعاين الإصلاح قبل قبوله. لكن فيه مصيدة: --diff يستلزم --fix-only ضمناً، فيخرج بالرمز 0 ما دام لا فرق يُطبع، حتى لو كانت في الشفرة مخالفات لا إصلاح آلي لها. لا تبنِ عليه بوابة CI إذاً؛ البوابة وظيفة ruff check المجرّد الذي يخرج بالرمز 1 عند أي مخالفة، ومعه ruff format --check. و--show-settings يطبع الإعدادات التي سيستخدمها Ruff لملف بعينه، وهو أقصر طريق لفهم سلوك غير متوقّع، لأن Ruff لا يدمج ملفات الإعداد: يأخذ أقربها ويتجاهل ما فوقه.

إصلاحات آمنة وأخرى تغيّر سلوك برنامجك

الإصلاح الآمن يُطبَّق افتراضياً مع --fix ويحافظ على السلوك. غير الآمن لا يعمل إلا بـ--unsafe-fixes، والفرق ليس شكلياً. خذ القاعدة RUF015، والشفرة عندك هكذا:

head = list(rows)[0]

فيحوّلها الإصلاح إلى:

head = next(iter(rows))

التوثيق يصنّفه غير آمن لسببين. الأول أن البناء الأصلي يقيّم المجموعة كاملة بشغف بينما next(iter(...)) يقيّم أول عنصر فقط، فتتأخر الآثار الجانبية أو لا تقع أصلاً. والثاني أن الوصول بالأقواس المربعة أو بـpop() يرفع IndexError على مجموعة فارغة بينما next(iter(...)) يرفع StopIteration — أي أن try/except IndexError المحيط بالسطر الأول يصير شفرة ميتة، ويمرّ الاستثناء من فوق المعالج الذي كتبته له.

وليست حالة شاذة: حذف متغيّر غير مستخدم في F841 غير آمن لأنه قد يبتلع تعليقاً مرتبطاً بالإسناد، وتحويل typing.List[int] إلى list[int] في UP006 غير آمن لأن مكتبات تقرأ التعليقات التوضيحية وقت التشغيل قد تنكسر به. راجع --diff قبل أي إصلاح غير آمن، ولا تطلقه على الشجرة كاملة دفعة واحدة؛ ولضبط أدقّ يعيد extend-safe-fixes وextend-unsafe-fixes تصنيف قواعد بعينها بدل حكم واحد على الكل.

قواعد تتعارك مع المنسّق فعطّلها من البداية

يسرد التوثيق قواعد فحص لا معنى لتفعيلها مع ruff format لأنها تنازعه القرار نفسه: W191 للمسافات البادئة بالتبويب، وE111 وE114 وE117، وD203 وD206 وD300، وعائلة الاقتباسات Q000Q004، وCOM812 وCOM819 للفاصلة الأخيرة، وISC002. تفعيلها يعني تحذيراً يتكرر في كل تشغيل بعد أن يكون المنسّق قد كتب السطر على هواه.

وE501 حالة خاصة. هي خارج المجموعة الافتراضية أصلاً، فلا تصل إليك إلا إن فعّلت عائلة E كاملة — وعندها تكتشف أن المنسّق يبذل «جهداً أفضلياً» في لفّ السطور عند line-length ولا يضمنه: سلسلة نصية طويلة، أو رابط داخل docstring، أو تعليق لا يقبل الكسر. فيبقى تحذير طول سطر لا يستطيع المنسّق إسكاته. إن فعّلت عائلات واسعة مثل E أو D أو COM، أدرج القواعد المتنازعة في ignore من الالتزام الأول.

ترتيب الاستيراد لا يتولّاه المنسّق

ruff format لا يرتّب الاستيراد إطلاقاً؛ ذلك عمل القاعدة I في الفاحص. لهذا ينصّ التوثيق على استدعاء الفاحص أولاً ثم المنسّق:

ruff check --select I --fix
ruff format

والترتيب ليس تفصيلاً: إعادة ترتيب الاستيراد قد تُخرج كتلة تحتاج إعادة تنسيق، فتشغيل المنسّق أخيراً يضمن استقرار الملف. وفي [tool.ruff.lint.isort] مفتاحان يحسمان معظم الشكاوى: known-first-party وافتراضه []، وفيه تُعرّف حزم مشروعك حتى لا تُصنَّف طرفاً ثالثاً؛ وcombine-as-imports وافتراضه false، ويدمج استيرادات as في جملة واحدة.

خطة هجرة على أربع دفعات

  1. قِس قبل أن تعدّل. شغّل ruff check --statistics على الشجرة كما هي لتعرف حجم الدَّين وتوزيعه. ثم فعّل E, F, I فقط، وشغّل ruff check --fix . && ruff format . في التزام تنسيق واحد لا يحمل أي تغيير منطقي، وسجّل تجزئته في .git-blame-ignore-revs حتى لا يبتلع git blame تاريخ الملفات.
  2. أضِف B ثم UP، كل عائلة في التزام منفصل. bugbear يكشف أخطاء منطقية تستحق قراءة بشرية، وpyupgrade يحدّث صياغات قديمة؛ فصلهما يبقي كل دفعة قابلة للمراجعة.
  3. أضِف SIM وC4. هاتان أقرب إلى الأسلوب وإعادة الصياغة، ومكانهما بعد أن يستقر الفريق على ما سبق.
  4. احذف القديم أخيراً. امسح .flake8، وقسمي [tool.black] و[tool.isort]، وخطافاتها من pre-commit. الحذف خطوة أخيرة لا أولى، لتبقى لك شبكة أمان طوال الطريق.

والملف القديم الذي لا وقت لإصلاحه يُعالَج بـruff check --add-noqa عليه وحده، لا بتعطيل القاعدة على مستوى المشروع: تظل القاعدة فعّالة على كل شفرة جديدة، ويبقى الدَّين مرئياً عند سطوره بدل أن يختفي في ملف الإعداد. وحين تنظّف تلك الملفات لاحقاً، فعّل RUF100 ليكشف تعليقات noqa التي لم تعد تكبح شيئاً ويحذفها بـ--fix.

pre-commit وCI: الترتيب الذي يمنع فشلاً كاذباً

المستودع الرسمي هو astral-sh/ruff-pre-commit، والخطافان اللذان تريدهما ruff-check وruff-format (وخطاف ruff المجرّد اسم قديم للتوافق يشغّل الفحص وحده؛ إن وجدته في إعدادك فحدّثه). والقاعدة الموثّقة صريحة: مع --fix يجب أن يأتي خطاف الفحص قبل خطاف المنسّق — وقبل black وisort وأي منسّق آخر ما زال عندك — لأن إصلاحات Ruff قد تُخرج شفرة تحتاج إعادة تنسيق، فتفشل الدفعة بلا سبب حقيقي. أما بلا --fix فالترتيب لا يهم.

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.16.4
    hooks:
      - id: ruff-check
        args: [--fix]
      - id: ruff-format

وثبّت rev على إصدار ثانوي بعينه: Ruff يستخدم رقم الإصدار الثانوي للتغييرات الكاسرة ورقم التصحيح لإصلاح العلل، لأنه لم يعلن واجهة مستقرة بعد. وقفزة الافتراضي من 59 قاعدة إلى 413 في 0.16.0 هي المثال الحيّ على ثمن الإهمال هنا: مشروع لم يكتب select صراحةً وترك latest يستيقظ على مئات المخالفات في بناء لم يتغيّر فيه سطر واحد من شفرته. ولاستثناء دفاتر Jupyter من الخطافين استخدم types_or: [python, pyi].

وفي CI خياران. الأول تشغيل ruff check --output-format=github . بنفسك، وهو ما شرحنا أثره في GitHub Actions لمشاريع بايثون (ولمستخدمي GitLab نظيره --output-format=gitlab). والثاني الأكشن الرسمي astral-sh/ruff-action، الذي يقبل args وsrc وversion وchecksum ويضيف ruff إلى PATH لخطوات لاحقة؛ ولفحص التنسيق مرّر له args: "format --check".

وهنا التباس يستحق التنبيه: الأكشن لا يُنتج تلك الملاحظات المضمّنة من تلقاء نفسه، لأن args الافتراضية هي check وحدها؛ إن أردتها فمرّر الراية بنفسك. وانتبه أيضاً إلى أن توثيق Ruff ما زال يعرض أمثلة بـruff-action@v3 بينما المستودع انتقل إلى الإصدار الرابع — انسخ من المستودع لا من صفحة التوثيق.

من أين تبدأ حسب حالة مشروعك

حالة المشروعمن أين تبدأما تؤجّله
مشروع جديد بلا شفرة قديمةخذ الافتراضي الموسّع كما هو على إصدار مثبّت، وأضِف خطافي pre-commit معاً من أول التزامPL وD حتى يستقر أسلوب الفريق ويتضح ما يحتاج توثيقاً فعلاً
مشروع قائم صغير عليه black وisort فقطruff format . ثم ruff check --select I --fix في التزام تنسيق واحد، ثم فعّل E, Fحذف [tool.black] و[tool.isort] حتى يمرّ أسبوع بلا فروق تنسيق مفاجئة
مشروع قديم كبير بآلاف المخالفاتruff check --statistics أولاً، ثم select = ["F"] وحدها، و--add-noqa للملفات المتروكةعائلة E الواسعة و--unsafe-fixes وأي عائلة أسلوبية حتى تنظف F بالكامل
مشروع فيه فريق ومراجعات pull requestruff check --output-format=github . بوابةً في CI، والاتفاق على per-file-ignores للاختباراتتشغيل --fix تلقائياً داخل CI؛ اترك الإصلاح على جهاز المطوّر ليمرّ بالمراجعة

أين يقف Ruff عند الحدّ

حدّان يبقيان بعد أن يخضرّ كل شيء. الأول معروف: Ruff مدقّق لا مدقّق أنواع، فأبقِ mypy أو Pyright بجانبه (وأداة ty من Astral ما زالت في بيتا، فلا تبنِ عليها بوابة إلزامية اليوم).

والثاني أهدأ وأخطر، وهو ما يُسقط الهجرات فعلاً: أي إضافة flake8 لم يُعَد تنفيذ قواعدها داخل Ruff تسقط بلا صوت — لا رسالة خطأ ولا تحذير «قاعدة غير معروفة»، فقط فحص كان يعمل أمس وتوقف اليوم. وتغطية pylint خصوصاً ليست كاملة، وRuff لا يقبل قواعد مخصّصة يكتبها فريقك ولا نظام إضافات من طرف ثالث. فقبل أن تحذف سطراً من ملف متطلباتك، ابحث عن قواعد تلك الأداة بالاسم في قائمة قواعد Ruff؛ دقيقة بحث هنا أرخص من ثغرة تمرّ إلى الإنتاج بعد شهر.

وأخيراً: إدارة الحزم والبيئات مسار مستقل تماماً عن هذا، خطواته في أداة uv لإدارة حزم بايثون. لا تدمج الهجرتين في التزام واحد، فتخلط انكسار الفحص بانكسار التثبيت وتفقد القدرة على معرفة أيهما السبب.