Kod sifati: ruff, formatlash va CI
Muhokamani avtomatlashtirish
Kod ko'rigida ikki xil izoh bo'ladi:
| Tur | Misol | Kim aytishi kerak |
|---|---|---|
| Mexanik | "bu yerda bo'sh joy ortiqcha", "import ishlatilmayapti" | Asbob |
| Mazmuniy | "bu funksiya ikki ish qilyapti", "bu chegara xato" | Odam |
Agar odam mexanik narsalarni aytsa, u mazmunga vaqt ajratmaydi. Shuning uchun birinchisini butunlay avtomatlashtirish kerak.
ruff: linter va formatlovchi
ruff — Rust da yozilgan, shuning uchun juda tez. U flake8, isort, pyupgrade va boshqa
o'nlab asbobning o'rnini bosadi.
shu faylni yaratingruff.toml
line-length = 100
target-version = "py310"
[lint]
select = [
"E", "W", # pycodestyle: bo'sh joy, uzunlik
"F", # pyflakes: ishlatilmagan import/o'zgaruvchi, noto'g'ri nom
"I", # isort: import tartibi
"B", # bugbear: keng tarqalgan xatolar
"UP", # pyupgrade: eskirgan yozuvlar
"SIM", # soddalashtirish mumkin bo'lgan joylar
"RUF", # ruff ning o'z qoidalari
]
ignore = ["E501"] # uzunlikni formatlovchi o'zi hal qiladi
[lint.per-file-ignores]
"tests/*" = ["S101"] # testda assert — normal
[format]
quote-style = "double"
Ataylab muammoli fayl yozamiz:
shu faylni yaratingyomon_uslub.py
import os import sys import json from typing import List, Optional def hisobla( sonlar:List[int], koef:Optional[int]=None ) ->int: natija=0 for i in range( len(sonlar) ): natija = natija + sonlar[i] if koef != None: natija = natija*koef unused = "hech qayerda ishlatilmaydi" if natija > 0: return natija else: return 0 def toplam(elementlar=[]): elementlar.append(1) return elementlar
ruff check yomon_uslub.py
I001 [*] Import block is un-sorted or un-formatted --> yomon_uslub.py:1:1 | 1 | / import os 2 | | import sys 3 | | import json 4 | | from typing import List, Optional | |_________________________________^ | help: Organize imports F401 [*] `os` imported but unused --> yomon_uslub.py:1:8 | 1 | import os | ^^ 2 | import sys 3 | import json | help: Remove unused import: `os` F401 [*] `sys` imported but unused --> yomon_uslub.py:2:8 | 1 | import os 2 | import sys | ^^^ 3 | import json 4 | from typing import List, Optional | help: Remove unused import: `sys` F401 [*] `json` imported but unused --> yomon_uslub.py:3:8 | 1 | import os 2 | import sys 3 | import json | ^^^^ 4 | from typing import List, Optional | help: Remove unused import: `json` UP035 `typing.List` is deprecated, use `list` instead --> yomon_uslub.py:4:1 | 2 | import sys 3 | import json 4 | from typing import List, Optional | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | UP006 [*] Use `list` instead of `List` for type annotation --> yomon_uslub.py:7:21 | 7 | def hisobla( sonlar:List[int], koef:Optional[int]=None ) ->int: | ^^^^ 8 | natija=0 9 | for i in range( len(sonlar) ): | help: Replace with `list` UP045 [*] Use `X | None` for type annotations --> yomon_uslub.py:7:37 | 7 | def hisobla( sonlar:List[int], koef:Optional[int]=None ) ->int: | ^^^^^^^^^^^^^ 8 | natija=0 9 | for i in range( len(sonlar) ): | help: Convert to `X | None` E711 Comparison to `None` should be `cond is not None` --> yomon_uslub.py:11:16 | 9 | for i in range( len(sonlar) ): 10 | natija = natija + sonlar[i] 11 | if koef != None: | ^^^^ 12 | natija = natija*koef 13 | unused = "hech qayerda ishlatilmaydi" | help: Replace with `cond is not None` F841 Local variable `unused` is assigned to but never used --> yomon_uslub.py:13:5 | 11 | if koef != None: 12 | natija = natija*koef 13 | unused = "hech qayerda ishlatilmaydi" | ^^^^^^ 14 | if natija > 0: 15 | return natija | help: Remove assignment to unused variable `unused` B006 Do not use mutable data structures for argument defaults --> yomon_uslub.py:20:23 | 20 | def toplam(elementlar=[]): | ^^ 21 | elementlar.append(1) 22 | return elementlar | help: Replace with `None`; initialize within function Found 10 errors. [*] 6 fixable with the `--fix` option (3 hidden fixes can be enabled with the `--unsafe-fixes` option).
Har xato kodi bilan ko'rsatildi (F401, E225, ...), shuning uchun kerak bo'lsa qoidani
o'chirish oson.
Ba'zilarini ruff o'zi tuzatadi:
ruff check --fix yomon_uslub.py echo "--- tuzatishdan keyin ---" cat yomon_uslub.py
E711 Comparison to `None` should be `cond is not None`
--> yomon_uslub.py:7:16
|
5 | for i in range( len(sonlar) ):
6 | natija = natija + sonlar[i]
7 | if koef != None:
| ^^^^
8 | natija = natija*koef
9 | unused = "hech qayerda ishlatilmaydi"
|
help: Replace with `cond is not None`
F841 Local variable `unused` is assigned to but never used
--> yomon_uslub.py:9:5
|
7 | if koef != None:
8 | natija = natija*koef
9 | unused = "hech qayerda ishlatilmaydi"
| ^^^^^^
10 | if natija > 0:
11 | return natija
|
help: Remove assignment to unused variable `unused`
B006 Do not use mutable data structures for argument defaults
--> yomon_uslub.py:16:23
|
16 | def toplam(elementlar=[]):
| ^^
17 | elementlar.append(1)
18 | return elementlar
|
help: Replace with `None`; initialize within function
Found 10 errors (7 fixed, 3 remaining).
No fixes available (3 hidden fixes can be enabled with the `--unsafe-fixes` option).
--- tuzatishdan keyin ---
def hisobla( sonlar:list[int], koef:int | None=None ) ->int:
natija=0
for i in range( len(sonlar) ):
natija = natija + sonlar[i]
if koef != None:
natija = natija*koef
unused = "hech qayerda ishlatilmaydi"
if natija > 0:
return natija
else:
return 0
def toplam(elementlar=[]):
elementlar.append(1)
return elementlar
echo "--- qolgan muammolar ---" ruff check yomon_uslub.py
--- qolgan muammolar --- E711 Comparison to `None` should be `cond is not None` --> yomon_uslub.py:7:16 | 5 | for i in range( len(sonlar) ): 6 | natija = natija + sonlar[i] 7 | if koef != None: | ^^^^ 8 | natija = natija*koef 9 | unused = "hech qayerda ishlatilmaydi" | help: Replace with `cond is not None` F841 Local variable `unused` is assigned to but never used --> yomon_uslub.py:9:5 | 7 | if koef != None: 8 | natija = natija*koef 9 | unused = "hech qayerda ishlatilmaydi" | ^^^^^^ 10 | if natija > 0: 11 | return natija | help: Remove assignment to unused variable `unused` B006 Do not use mutable data structures for argument defaults --> yomon_uslub.py:16:23 | 16 | def toplam(elementlar=[]): | ^^ 17 | elementlar.append(1) 18 | return elementlar | help: Replace with `None`; initialize within function Found 3 errors. No fixes available (3 hidden fixes can be enabled with the `--unsafe-fixes` option).
Avtomatik tuzatilmagan ikkitasi eng muhimi. Ularni ko'rib chiqamiz.
B006: o'zgaruvchan sukut argumenti
Bu Python dagi eng mashhur tuzoq:
def toplam(elementlar=[]): elementlar.append(1) return elementlar print("1-chaqiruv:", toplam()) print("2-chaqiruv:", toplam()) print("3-chaqiruv:", toplam()) print("\nsabab: sukut qiymati FUNKSIYA E'LON QILINGANDA bir marta yaratiladi") print("id har safar bir xil:", id(toplam.__defaults__[0]))
1-chaqiruv: [1] 2-chaqiruv: [1, 1] 3-chaqiruv: [1, 1, 1] sabab: sukut qiymati FUNKSIYA E'LON QILINGANDA bir marta yaratiladi id har safar bir xil: 139659140556352
Ro'yxat chaqiruvlar orasida saqlanib qoldi. To'g'ri yozuv:
def toplam_togri(elementlar=None): if elementlar is None: elementlar = [] elementlar.append(1) return elementlar print("1-chaqiruv:", toplam_togri()) print("2-chaqiruv:", toplam_togri()) print("3-chaqiruv:", toplam_togri())
1-chaqiruv: [1] 2-chaqiruv: [1] 3-chaqiruv: [1]
ruff buni B006 bilan ushladi — bu haqiqiy xato, uslub masalasi emas.
E711: != None
class Gaalati: def __eq__(self, boshqa): return True g = Gaalati() print("g == None ->", g == None) # __eq__ chaqiriladi print("g is None ->", g is None) # obyekt o'ziga solishtiriladi print("\nshuning uchun har doim `is None` / `is not None`")
g == None -> True g is None -> False shuning uchun har doim `is None` / `is not None`
== None obyektning __eq__ metodini chaqiradi va u yolg'on gapirishi mumkin. is None esa
obyekt aynan None ekanini tekshiradi.
Formatlash
shu faylni yaratingformatsiz.py
def hisobla(a,b, c): natija={'birinchi':a, 'ikkinchi':b, 'uchinchi':c} if a>b and b>c: return natija return { }
echo "--- formatlashdan oldin ---" cat formatsiz.py ruff format formatsiz.py echo "--- formatlashdan keyin ---" cat formatsiz.py
--- formatlashdan oldin ---
def hisobla(a,b, c):
natija={'birinchi':a, 'ikkinchi':b,
'uchinchi':c}
if a>b and b>c:
return natija
return { }
1 file reformatted
--- formatlashdan keyin ---
def hisobla(a, b, c):
natija = {"birinchi": a, "ikkinchi": b, "uchinchi": c}
if a > b and b > c:
return natija
return {}
Formatlovchining asosiy foydasi — muhokamani tugatish. Bo'sh joy, qavs, qator uzunligi haqida bahslashish shart emas: asbob qaror qiladi, hamma bo'ysunadi.
rm -f yomon_uslub.py formatsiz.py
(chiqish yo'q)
Loyihani tekshiramiz
echo "=== linter nima deydi ===" ruff check src tests echo "" echo "=== formatlash kerakmi ===" ruff format --check src tests 2>&1 | tail -3
=== linter nima deydi === RUF022 [*] `__all__` is not sorted --> src/hisobot/__init__.py:9:11 | 8 | __version__ = "0.4.0" 9 | __all__ = ["daraja_taqsimoti", "guruh_ortachasi", "Daraja", "Natija", "Talaba", | ___________^ 10 | | "oqi", "tozala", "bahoni_tekshir", "__version__"] | |____________________________________________________________^ | help: Apply an isort-style sorting to `__all__` F401 [*] `.oqish.oqi` imported but unused --> src/hisobot/cli.py:11:20 | 9 | from .hisob import daraja_taqsimoti, eng_yaxshi, guruh_ortachasi 10 | from .ko_p import hammasi 11 | from .oqish import oqi, tozala | ^^^ 12 | from .sozlama import Sozlama 13 | from .xatolar import HisobotXato, SozlamaXato | help: Remove unused import F401 [*] `.oqish.tozala` imported but unused --> src/hisobot/cli.py:11:25 | 9 | from .hisob import daraja_taqsimoti, eng_yaxshi, guruh_ortachasi 10 | from .ko_p import hammasi 11 | from .oqish import oqi, tozala | ^^^^^^ 12 | from .sozlama import Sozlama 13 | from .xatolar import HisobotXato, SozlamaXato | help: Remove unused import F401 [*] `.xatolar.HisobotXato` imported but unused --> src/hisobot/cli.py:13:22 | 11 | from .oqish import oqi, tozala 12 | from .sozlama import Sozlama 13 | from .xatolar import HisobotXato, SozlamaXato | ^^^^^^^^^^^ 14 | 15 | log = logging.getLogger(__name__) | help: Remove unused import: `.xatolar.HisobotXato` Found 4 errors. [*] 4 fixable with the `--fix` option. === formatlash kerakmi === Would reformat: tests/test_tarmoq.py Would reformat: tests/test_tekshiruv.py 18 files would be reformatted, 9 files already formatted
Loyihamiz toza emas: linter __init__.py dagi tartibsizlikni topdi va deyarli hamma fayl
formatlanmagan. Buni bir marta tuzatamiz:
ruff check --fix src tests ruff format src tests
Found 4 errors (4 fixed, 0 remaining). 18 files reformatted, 9 files left unchanged
echo "=== endi qayta tekshiramiz ===" ruff check src tests ruff format --check src tests echo "qaytish kodi: $?"
=== endi qayta tekshiramiz === All checks passed! 27 files already formatted qaytish kodi: 0
Endi ikkalasi ham toza. --check fayllarni o'zgartirmaydi, faqat "formatlash kerakmi?" deb aytadi —
CI da aynan shu ishlatiladi.
:::gotcha Formatlashdan keyin testlarni qayta chopting
ruff check --fix kodni o'zgartiradi. Odatda bu xavfsiz, lekin har doim emas. Shuning uchun
tuzatishdan keyin testlarni albatta qayta ishga tushiring:
.venv/bin/python3 -m pytest -q python3 -m mypy
........................................................................ [ 86%] ........... [100%] src/hisobot/cli.py:127: error: Function is missing a type annotation for one or more parameters [no-untyped-def] Found 1 error in 1 file (checked 13 source files)
:::
Uchta asbob, uchta vazifa
import pandas as pd show(pd.DataFrame([ {"asbob": "ruff format", "nima qiladi": "Ko'rinish: bo'sh joy, qator uzunligi, qavslar", "xato topadimi": "yo'q"}, {"asbob": "ruff check", "nima qiladi": "Uslub + keng tarqalgan xatolar (B006, F401)", "xato topadimi": "ba'zilarini"}, {"asbob": "mypy", "nima qiladi": "Turlar mosligini tekshiradi", "xato topadimi": "ha, mantiqiy xatolarni"}, {"asbob": "pytest", "nima qiladi": "Xulq to'g'riligini tekshiradi", "xato topadimi": "ha, asosiysini"}, ]))
| asbob | nima qiladi | xato topadimi |
|---|---|---|
| ruff format | Ko'rinish: bo'sh joy, qator uzunligi, qavslar | yo'q |
| ruff check | Uslub + keng tarqalgan xatolar (B006, F401) | ba'zilarini |
| mypy | Turlar mosligini tekshiradi | ha, mantiqiy xatolarni |
| pytest | Xulq to'g'riligini tekshiradi | ha, asosiysini |
To'rttasi bir-birini almashtirmaydi. Har biri boshqa turdagi muammoni topadi.
Bitta buyruqqa yig'amiz
shu faylni yaratingtekshir.sh
#!/usr/bin/env bash
# Loyihaning barcha tekshiruvlari. CI ham, dasturchi ham shu bitta buyruqni ishlatadi.
set -u
XATO=0
ishga() {
echo ""
echo "=== $1 ==="
shift
if "$@"; then
echo "OK"
else
echo "YIQILDI (kod $?)"
XATO=1
fi
}
ishga "format" ruff format --check src tests
ishga "linter" ruff check src tests
ishga "turlar" python3 -m mypy
ishga "testlar" .venv/bin/python3 -m pytest -q
echo ""
if [ "$XATO" -eq 0 ]; then
echo "HAMMASI O'TDI"
else
echo "BA'ZI TEKSHIRUVLAR YIQILDI"
fi
exit "$XATO"
chmod +x tekshir.sh ./tekshir.sh echo "" echo "umumiy qaytish kodi: $?"
=== format === 27 files already formatted OK === linter === All checks passed! OK === turlar === src/hisobot/cli.py:127: error: Function is missing a type annotation for one or more parameters [no-untyped-def] Found 1 error in 1 file (checked 13 source files) YIQILDI (kod 1) === testlar === ........................................................................ [ 86%] ........... [100%] OK BA'ZI TEKSHIRUVLAR YIQILDI umumiy qaytish kodi: 0
Muhim tafsilot: skript birinchi xatoda to'xtamaydi. Sababi — dasturchi hamma muammoni bir marta ko'rishi kerak, birma-bir emas.
Uzluksiz integratsiya (CI)
shu faylni yarating.github/workflows/tekshir.yml
name: tekshiruvlar
on:
push:
branches: [main]
pull_request:
jobs:
sinov:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: pip
- name: bog'liqliklarni o'rnatish
run: pip install -e ".[dev]"
- name: format
run: ruff format --check src tests
- name: linter
run: ruff check src tests
- name: turlar
run: mypy
- name: testlar
run: pytest --cov=hisobot --cov-report=term-missing --cov-fail-under=75
ls -la .github/workflows/ .venv/bin/python3 -c " import re, pathlib matn = pathlib.Path('.github/workflows/tekshir.yml').read_text(encoding='utf-8') qadamlar = re.findall(r'- name: (.+)', matn) versiyalar = re.search(r'python: \[(.+)\]', matn).group(1) print('Python versiyalari:', versiyalar) print('qadamlar:', qadamlar) "
total 1 drwxrwxrwx 1 abulqosim abulqosim 512 Sep 29 14:01 . drwxrwxrwx 1 abulqosim abulqosim 512 Sep 29 14:01 .. -rwxrwxrwx 1 abulqosim abulqosim 713 Sep 29 14:01 tekshir.yml Python versiyalari: "3.10", "3.11", "3.12" qadamlar: ["bog'liqliklarni o'rnatish", 'format', 'linter', 'turlar', 'testlar']
matrix uch versiyada sinaydi. fail-fast: false — bittasi yiqilsa ham qolganlari ishlaydi,
shunda muammo bitta versiyagami yoki hammasidami — darhol ma'lum.
pre-commit: commit dan oldin
CI xatoni yuborgandan keyin topadi. pre-commit esa commit paytida:
shu faylni yarating.pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
args: [--maxkb=500]
- id: detect-private-key
detect-private-key alohida qimmatli: u maxfiy kalit tasodifan commit ga tushishini to'xtatadi.
11-bobdagi qoidani eslang — ochilgan sirni o'chirish yetarli emas.
O'rnatish: pre-commit install. Shundan keyin har git commit da avtomatik ishlaydi.
:::tip Formatlovchini keyin qo'shsangiz
Mavjud loyihaga formatlovchi qo'shsangiz, birinchi ishga tushirish butun kodni o'zgartiradi
va git tarixi buziladi. Yechim: formatlashni alohida commit qiling va uning hash ini
.git-blame-ignore-revs fayliga yozing. Shunda git blame uni o'tkazib yuboradi.
:::
Sifatni raqamda o'lchash
.venv/bin/python3 -m pytest --cov=hisobot --cov-report=term --quiet 2>&1 | tail -15 echo "" echo "--- kod hajmi ---" find src tests -name "*.py" | xargs wc -l | tail -1 echo "--- fayllar soni ---" find src -name "*.py" | wc -l echo "--- testlar soni ---" .venv/bin/python3 -m pytest --collect-only -q 2>/dev/null | tail -2
src/hisobot/__init__.py 6 0 100% src/hisobot/__main__.py 2 2 0% 3-5 src/hisobot/chiqarish.py 22 0 100% src/hisobot/cli.py 85 4 95% 22, 106-107, 120 src/hisobot/hisob.py 12 0 100% src/hisobot/ko_p.py 22 0 100% src/hisobot/modellar.py 33 1 97% 21 src/hisobot/muhit.py 17 17 0% 3-26 src/hisobot/oqish.py 42 4 90% 25-28 src/hisobot/sozlama.py 39 1 97% 59 src/hisobot/tarmoq.py 30 4 87% 41-44 src/hisobot/tekshiruv.py 22 0 100% src/hisobot/xatolar.py 13 4 69% 24-27 -------------------------------------------------------- TOTAL 345 37 89% --- kod hajmi --- 1096 total --- fayllar soni --- 13 --- testlar soni --- tests/test_xatolar.py: 6
Bu raqamlarni kuzatib borish foydali: qamrov tushib ketsa yoki fayl 800 qatordan oshsa — signal.
Lekin ehtiyot bo'ling: raqam maqsadga aylansa, u o'lchov bo'lishdan to'xtaydi. 100% qamrov talab qilsangiz, odamlar bo'sh test yozadi (7-bobda ko'rdik).
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| Mexanik izohlar | Asbob aytsin, odam emas |
ruff check |
Linter: F401, B006, E711 — ba'zilari haqiqiy xato |
ruff format |
Ko'rinish haqidagi bahsni tugatadi |
B006 |
O'zgaruvchan sukut argumenti chaqiruvlar orasida saqlanadi |
is None |
== None obyektning __eq__ iga tayanadi — ishonchsiz |
| To'rt asbob | format, linter, turlar, testlar — biri boshqasini almashtirmaydi |
| Bitta skript | tekshir.sh — birinchi xatoda to'xtamasin |
| CI matrix | Bir necha Python versiyasida sinang |
pre-commit |
Xatoni yuborishdan oldin ushlaydi; detect-private-key muhim |
Talaba ishi
ruff.tomlga"ANN"(annotatsiyalar) qoidasini qo'shing. Nechta yangi ogohlantirish chiqdi? Ularning hammasi foydalimi?tekshir.shga qamrov chegarasini qo'shing (--cov-fail-under=75). Loyiha o'tadimi?ruff check --statistics src testsni ishga tushiring. Qaysi qoida eng ko'p buzilgan?B006xatosini o'z kodingizda qidiring:grep -rn "=\[\]\|={}" src/. Topildimi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.