IlmHamroh
Python kursi/Standart kutubxona15/16-dars23 daqiqa
Mundarija (21)

15.15-dars: argparse — CLI

15-QISM — STANDART KUTUBXONA · 15-dars


1. Kirish va motivatsiya

15.11-darsda sys.argv ni ko'rdik: argumentlar oddiy satrlar ro'yxati. Kichik skript uchun sys.argv[1] yetadi. Lekin haqiqiy buyruq qatori dasturida savollar ko'payadi:

  • Qaysi argument majburiy, qaysi ixtiyoriy?
  • -n 5, --soni=5, -n5 — hammasi bir xil ishlashi kerak.
  • "besh" kiritilsa — tushunarli xato va 2 chiqish kodi.
  • --help avtomatik, har argumentning tavsifi va sukut qiymati bilan.
  • git commit, git push kabi ichki buyruqlar.

Bularni qo'lda yozish — yuzlab qator va har dasturda boshqacha xulq. argparse standart kutubxonada va bularning hammasini beradi.

Real vaziyat. Ma'lumotlarni import qilish skripti --tozala bayrog'ini shunday e'lon qilgan:

python
parser.add_argument("--tozala", type=bool, default=False)

Operator ehtiyotkorlik uchun --tozala False deb yozdi. Skript jadvalni tozaladi: bool("False") — True. Boshqa jamoaning CLI'sida esa sys.argv qo'lda tahlil qilinardi va noma'lum argumentlar jim e'tiborsiz qolardi — --quruq-rejim o'rniga --quruq_rejim yozilgani uchun "quruq" ishga tushirish haqiqiy o'chirishga aylandi.

Bu darsda argparse ni to'liq o'rganamiz va testlanadigan CLI dastur quramiz.

Bu darsda:

  • Pozitsion va ixtiyoriy argumentlar, type, default, choices, required
  • Amallar: store_true, count, append, nargs, BooleanOptionalAction
  • type=bool tuzog'i
  • O'z tur funksiyasi va ArgumentTypeError
  • O'zaro istisno qiluvchi guruhlar
  • Ichki buyruqlar: add_subparsers, set_defaults, umumiy (parent) parserlar
  • Xatolar: chiqish kodi 2, exit_on_error=False, 3.14 suggest_on_error
  • Testlanadigan tuzilma: main(argv) -> int
  • Amaliy: vazifalar menejeri CLI

2. Nazariya — chuqur tushuntirish

2.1. Asosiy tuzilma

python
import argparse

parser = argparse.ArgumentParser(prog="import", description="CSV ni bazaga import qilish")
parser.add_argument("fayl")                                   # pozitsion — majburiy
parser.add_argument("-n", "--soni", type=int, default=100)    # ixtiyoriy
parser.add_argument("--format", choices=["csv", "tsv"], default="csv")
args = parser.parse_args()          # sys.argv[1:] dan
args = parser.parse_args(["a.csv", "-n", "5"])   # ⭐ testlarda — aniq ro'yxat
args.fayl, args.soni
Tushuncha Qanday
Pozitsion "fayl" — nomi - siz, majburiy
Ixtiyoriy "-n", "--soni" — atribut nomi uzun shakldan: soni (- → _)
dest Atribut nomini aniq berish
required=True Ixtiyoriy argumentni majburiy qilish (kamdan-kam)
metavar Yordam matnidagi nom
help Tavsif; %(default)s sukut qiymatini qo'yadi

Yozish shakllari bir xil ishlaydi: -n 5, -n5, --soni 5, --soni=5. -- dan keyingi hamma narsa pozitsion (masalan, - bilan boshlanadigan fayl nomi).

2.2. type, default, choices

Parametr Xulq
type=int Satrni o'giradi; xato bo'lsa invalid int value va kod 2
type=Path, type=float Istalgan chaqiriladigan narsa
default="8000" Satr sukut ham type orqali o'tadi → int
choices=[...] Ro'yxatdan tashqari qiymat — xato

O'z tur funksiyasi:

python
def musbat_son(qiymat: str) -> int:
    son = int(qiymat)
    if son <= 0:
        raise argparse.ArgumentTypeError(f"musbat son kerak: {qiymat}")
    return son

2.3. Amallar (action) va nargs

Amal Misol Natija
store (sukut) --soni 5 5
store_true / store_false --quruq True
count -vvv 3
append --teg a --teg b ["a", "b"]
BooleanOptionalAction --rang / --no-rang True / False
version --version Versiyani chiqarib, chiqadi
nargs Ma'nosi
N (son) Aynan N ta
"?" 0 yoki 1 (const — argumentsiz berilganda)
"*" 0 yoki ko'p
"+" 1 yoki ko'p

type=bool tuzog'i: bool("False") — True. Mantiqiy bayroqlar uchun store_true yoki BooleanOptionalAction.

append bilan default=[] — ro'yxat qiymatlari sukutga qo'shiladi; sukut bo'sh bo'lmasa kutilmagan natija beradi.

2.4. Guruhlar

Guruh Maqsad
add_argument_group("Chiqish") Yordam matnida bo'limlar
add_mutually_exclusive_group() Bir vaqtda faqat bittasi: --json yoki --csv
add_mutually_exclusive_group(required=True) Aynan bittasi majburiy

2.5. Ichki buyruqlar

python
parser = argparse.ArgumentParser(prog="vazifa")
buyruqlar = parser.add_subparsers(dest="buyruq", required=True)

qosh = buyruqlar.add_parser("qosh", help="yangi vazifa")
qosh.add_argument("matn")
qosh.set_defaults(ishlovchi=vazifa_qosh)       # ⭐ har buyruqqa funksiya

args = parser.parse_args()
return args.ishlovchi(args)

Umumiy argumentlar — parents:

python
umumiy = argparse.ArgumentParser(add_help=False)
umumiy.add_argument("--fayl", type=Path)
buyruqlar.add_parser("qosh", parents=[umumiy])
Tafsilot Izoh
required=True Buyruqsiz ishga tushirish — xato (aks holda args.buyruq = None)
set_defaults(ishlovchi=...) if/elif zanjiri o'rniga
Umumiy bayroqlar Asosiy parserda — buyruqdan oldin; parents bilan — buyruqdan keyin

2.6. Xatolar

Holat Xulq
Noto'g'ri argument stderr ga usage va error: ..., kod 2 (SystemExit)
--help stdout ga yordam, kod 0
parse_known_args() Noma'lum argumentlarni xato qilmay, alohida ro'yxatda qaytaradi
exit_on_error=False Chiqish o'rniga ArgumentError — o'z xato ishlovingiz uchun
parser.error("xabar") O'z tekshiruvingiz xatosini standart shaklda chiqarish (kod 2)
suggest_on_error=True (3.14) choices va ichki buyruqlarda o'xshash variantni taklif qiladi: maybe you meant 'json'?
color (3.14) Yordam va xatolarni rangli chiqarish (terminalda)

Argumentlararo shartlar (masalan, "--oxiri --boshi dan keyin bo'lsin") argparse da yo'q — parse_args dan keyin tekshirib, parser.error(...) chaqiring.

2.7. Testlanadigan tuzilma

python
def parser_yarat() -> argparse.ArgumentParser: ...

def main(argv: list[str] | None = None) -> int:
    args = parser_yarat().parse_args(argv)     # None → sys.argv[1:]
    try:
        return args.ishlovchi(args)
    except FoydalanuvchiXatosi as x:
        print(f"xato: {x}", file=sys.stderr)
        return 1

if __name__ == "__main__":
    sys.exit(main())
Afzallik Nega
main(["qosh", "x"]) Test jarayon ishga tushirmasdan
int qaytaradi Chiqish kodi aniq (15.11)
Parser alohida funksiyada Yordam matnini testlash, hujjat yaratish
Sozlamalar muhitdan sukut default=os.environ.get("VAZIFA_FAYL", ...)

Katta CLI'lar uchun tashqi kutubxonalar: click, typer (tur annotatsiyalari asosida) — 19.6-dars.


3. Tez ma'lumotnoma

python
import argparse

p = argparse.ArgumentParser(prog="dastur", description="...")
p.add_argument("kirish", type=Path)
p.add_argument("-n", "--soni", type=int, default=10, help="soni (sukut: %(default)s)")
p.add_argument("-v", "--verbose", action="count", default=0)
p.add_argument("--quruq", action="store_true")
p.add_argument("--rang", action=argparse.BooleanOptionalAction, default=True)
p.add_argument("--format", choices=["json", "csv"], default="json")
g = p.add_mutually_exclusive_group(); g.add_argument("--tez"); g.add_argument("--aniq")

sub = p.add_subparsers(dest="buyruq", required=True)
b = sub.add_parser("qosh"); b.set_defaults(ishlovchi=qosh)

args = p.parse_args(argv)

Qoidalar

mantiqiy bayroq — store_true / BooleanOptionalAction, type=bool emas
main(argv) -> int — testlanadigan
buyruq — set_defaults(ishlovchi=...)
argumentlararo shart — parse_args dan keyin parser.error
noto'g'ri foydalanish — kod 2 (argparse o'zi)
prog ni aniq bering — yordam matni barqaror

4. Batafsil misollar

Misol 1 — Argumentlar va amallar

python
"""Pozitsion va ixtiyoriy; yozish shakllari; type, default, choices; store_true, count, append, nargs, BooleanOptionalAction; vars(); yordam matni."""

import argparse
from pathlib import Path


def parser_yarat() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(prog="import", description="CSV faylni bazaga import qilish", color=False)
    p.add_argument("fayl", type=Path, help="kirish fayli")
    p.add_argument("-n", "--soni", type=int, default="100", help="bir paketdagi qatorlar (sukut: %(default)s)")
    p.add_argument("-v", "--verbose", action="count", default=0, help="batafsil chiqish (-vv, -vvv)")
    p.add_argument("--quruq", action="store_true", help="bazaga yozmasdan tekshirish")
    p.add_argument("--rang", action=argparse.BooleanOptionalAction, default=True, help="rangli chiqish")
    p.add_argument("--format", choices=["csv", "tsv"], default="csv")
    p.add_argument("--ustun", action="append", default=[], metavar="NOM", help="faqat shu ustunlar (takrorlanadi)")
    p.add_argument("--sana", nargs=2, metavar=("BOSHI", "OXIRI"), help="sana oralig'i")
    p.add_argument("--kodlash", nargs="?", const="utf-8", default=None)
    return p


def main() -> None:
    p = parser_yarat()

    print("=== 1. Oddiy ===")
    args = p.parse_args(["sotuv.csv"])
    print(f"  {vars(args) | {'fayl': f'Path({args.fayl.as_posix()!r})'}}")
    print(f"  soni turi: {type(args.soni).__name__} (sukut '100' satr edi — type orqali o'tdi)")

    print("\n=== 2. Yozish shakllari bir xil ===")
    for argv in (["a.csv", "-n", "5"], ["a.csv", "-n5"], ["a.csv", "--soni", "5"], ["a.csv", "--soni=5"], ["-n", "5", "a.csv"]):
        print(f"  {' '.join(argv):20} → soni={p.parse_args(argv).soni}")

    print("\n=== 3. Amallar ===")
    args = p.parse_args(["a.csv", "-vvv", "--quruq", "--no-rang", "--ustun", "sana", "--ustun", "summa",
                         "--sana", "2026-09-01", "2026-09-30", "--kodlash"])
    print(f"  verbose={args.verbose}, quruq={args.quruq}, rang={args.rang}")
    print(f"  ustun={args.ustun}, sana={args.sana}, kodlash={args.kodlash!r}")
    print(f"  --kodlash berilmasa: {p.parse_args(['a.csv']).kodlash!r}, qiymat bilan: {p.parse_args(['a.csv', '--kodlash', 'cp1251']).kodlash!r}")

    print("\n=== 4. '--' dan keyin ===")
    print(f"  fayl nomi '-yashirin.csv': {p.parse_args(['--', '-yashirin.csv']).fayl}")

    print("\n=== 5. Yordam matni ===")
    for qator in p.format_help().splitlines():
        print(f"  {qator}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Oddiy ===
  {'fayl': "Path('sotuv.csv')", 'soni': 100, 'verbose': 0, 'quruq': False, 'rang': True, 'format': 'csv', 'ustun': [], 'sana': None, 'kodlash': None}
  soni turi: int (sukut '100' satr edi — type orqali o'tdi)

=== 2. Yozish shakllari bir xil ===
  a.csv -n 5           → soni=5
  a.csv -n5            → soni=5
  a.csv --soni 5       → soni=5
  a.csv --soni=5       → soni=5
  -n 5 a.csv           → soni=5

=== 3. Amallar ===
  verbose=3, quruq=True, rang=False
  ustun=['sana', 'summa'], sana=['2026-09-01', '2026-09-30'], kodlash='utf-8'
  --kodlash berilmasa: None, qiymat bilan: 'cp1251'

=== 4. '--' dan keyin ===
  fayl nomi '-yashirin.csv': -yashirin.csv

=== 5. Yordam matni ===
  usage: import [-h] [-n SONI] [-v] [--quruq] [--rang | --no-rang]
                [--format {csv,tsv}] [--ustun NOM] [--sana BOSHI OXIRI]
                [--kodlash [KODLASH]]
                fayl

  CSV faylni bazaga import qilish

  positional arguments:
    fayl                 kirish fayli

  options:
    -h, --help           show this help message and exit
    -n, --soni SONI      bir paketdagi qatorlar (sukut: 100)
    -v, --verbose        batafsil chiqish (-vv, -vvv)
    --quruq              bazaga yozmasdan tekshirish
    --rang, --no-rang    rangli chiqish
    --format {csv,tsv}
    --ustun NOM          faqat shu ustunlar (takrorlanadi)
    --sana BOSHI OXIRI   sana oralig'i
    --kodlash [KODLASH]

Nima ko'rsatdi: 2.1, 2.2, 2.3-bo'limlar.

Misol 2 — Xatolar va tuzoqlar

python
"""Xato xabarlari va kod 2; type=bool tuzog'i; ArgumentTypeError; o'zaro istisno guruhi; argumentlararo tekshiruv parser.error bilan; exit_on_error=False; parse_known_args; 3.14 suggest_on_error."""

import argparse
import contextlib
import io
from datetime import date


def xato_ushla(parser: argparse.ArgumentParser, argv: list[str]) -> str:
    oqim = io.StringIO()
    with contextlib.redirect_stderr(oqim):
        try:
            parser.parse_args(argv)
        except SystemExit as chiqish:
            return f"kod {chiqish.code}: {oqim.getvalue().strip().splitlines()[-1]}"
    return "xato yo'q"


def musbat_son(qiymat: str) -> int:
    try:
        son = int(qiymat)
    except ValueError:
        raise argparse.ArgumentTypeError(f"son kerak: {qiymat!r}") from None
    if son <= 0:
        raise argparse.ArgumentTypeError(f"musbat son kerak: {qiymat}")
    return son


def main() -> None:
    p = argparse.ArgumentParser(prog="hisobot", color=False)
    p.add_argument("fayl")
    p.add_argument("--soni", type=musbat_son, default=10)
    p.add_argument("--format", choices=["json", "csv"], default="json")
    p.add_argument("--boshi", type=date.fromisoformat)
    p.add_argument("--oxiri", type=date.fromisoformat)
    guruh = p.add_mutually_exclusive_group()
    guruh.add_argument("--jim", action="store_true")
    guruh.add_argument("--batafsil", action="store_true")

    print("=== 1. Standart xatolar (kod 2) ===")
    for argv in ([], ["a", "--soni", "besh"], ["a", "--soni", "-3"], ["a", "--format", "xml"],
                 ["a", "--nomalum"], ["a", "--jim", "--batafsil"], ["a", "--boshi", "17.09.2026"]):
        korinish = " ".join(argv) or "(bo'sh)"
        print(f"  {korinish:26} → {xato_ushla(p, argv)}")

    print("\n=== 2. ⚠️ type=bool ===")
    yomon = argparse.ArgumentParser(prog="import", color=False)
    yomon.add_argument("--tozala", type=bool, default=False)
    print(f"  --tozala False → {yomon.parse_args(['--tozala', 'False']).tozala}")
    print(f"  --tozala 0     → {yomon.parse_args(['--tozala', '0']).tozala}")
    yaxshi = argparse.ArgumentParser(prog="import", color=False)
    yaxshi.add_argument("--tozala", action=argparse.BooleanOptionalAction, default=False)
    print(f"  BooleanOptionalAction: --tozala → {yaxshi.parse_args(['--tozala']).tozala}, "
          f"--no-tozala → {yaxshi.parse_args(['--no-tozala']).tozala}, berilmasa → {yaxshi.parse_args([]).tozala}")

    print("\n=== 3. Argumentlararo tekshiruv ===")

    def tahlil(argv: list[str]) -> argparse.Namespace:
        args = p.parse_args(argv)
        if args.boshi and args.oxiri and args.oxiri < args.boshi:
            p.error(f"--oxiri ({args.oxiri}) --boshi ({args.boshi}) dan oldin bo'lishi mumkin emas")
        return args

    oqim = io.StringIO()
    with contextlib.redirect_stderr(oqim):
        try:
            tahlil(["a", "--boshi", "2026-09-30", "--oxiri", "2026-09-01"])
        except SystemExit as chiqish:
            print(f"  kod {chiqish.code}: {oqim.getvalue().strip().splitlines()[-1]}")

    print("\n=== 4. exit_on_error=False ===")
    boshqariladigan = argparse.ArgumentParser(prog="api", exit_on_error=False, color=False)
    boshqariladigan.add_argument("--port", type=int)
    try:
        boshqariladigan.parse_args(["--port", "sakkiz"])
    except argparse.ArgumentError as xato:
        print(f"  ArgumentError: {xato} — dastur o'zi hal qiladi (masalan, veb-forma)")

    print("\n=== 5. parse_known_args ===")
    args, qolgan = p.parse_known_args(["a", "--soni", "5", "--pytest-bayroq", "x"])
    print(f"  ma'lum: soni={args.soni}, noma'lum: {qolgan}")

    print("\n=== 6. 3.14: suggest_on_error ===")
    taklifli = argparse.ArgumentParser(prog="eksport", suggest_on_error=True, color=False)
    taklifli.add_argument("--format", choices=["json", "csv", "xlsx"])
    print(f"  {xato_ushla(taklifli, ['--format', 'jsn'])}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Standart xatolar (kod 2) ===
  (bo'sh)                    → kod 2: hisobot: error: the following arguments are required: fayl
  a --soni besh              → kod 2: hisobot: error: argument --soni: son kerak: 'besh'
  a --soni -3                → kod 2: hisobot: error: argument --soni: musbat son kerak: -3
  a --format xml             → kod 2: hisobot: error: argument --format: invalid choice: 'xml' (choose from 'json', 'csv')
  a --nomalum                → kod 2: hisobot: error: unrecognized arguments: --nomalum
  a --jim --batafsil         → kod 2: hisobot: error: argument --batafsil: not allowed with argument --jim
  a --boshi 17.09.2026       → kod 2: hisobot: error: argument --boshi: invalid fromisoformat value: '17.09.2026'

=== 2. ⚠️ type=bool ===
  --tozala False → True
  --tozala 0     → True
  BooleanOptionalAction: --tozala → True, --no-tozala → False, berilmasa → False

=== 3. Argumentlararo tekshiruv ===
  kod 2: hisobot: error: --oxiri (2026-09-01) --boshi (2026-09-30) dan oldin bo'lishi mumkin emas

=== 4. exit_on_error=False ===
  ArgumentError: argument --port: invalid int value: 'sakkiz' — dastur o'zi hal qiladi (masalan, veb-forma)

=== 5. parse_known_args ===
  ma'lum: soni=5, noma'lum: ['--pytest-bayroq', 'x']

=== 6. 3.14: suggest_on_error ===
  kod 2: eksport: error: argument --format: invalid choice: 'jsn', maybe you meant 'json'? (choose from 'json', 'csv', 'xlsx')

Nima ko'rsatdi: 2.2, 2.3, 2.4, 2.6-bo'limlar.

Misol 3 — Ichki buyruqlar

python
"""add_subparsers required; set_defaults bilan ishlovchi; parents bilan umumiy argumentlar; buyruqlarning o'z yordami; main(argv) -> int."""

import argparse
import contextlib
import io
import sys


def deploy(args: argparse.Namespace) -> int:
    print(f"  deploy: muhit={args.muhit}, versiya={args.versiya}, quruq={args.quruq}, verbose={args.verbose}")
    return 0


def qaytar(args: argparse.Namespace) -> int:
    if args.qadam > 3:
        print(f"  xato: {args.qadam} qadam orqaga — ko'pi bilan 3", file=sys.stderr)
        return 1
    print(f"  rollback: muhit={args.muhit}, qadam={args.qadam}")
    return 0


def holat(args: argparse.Namespace) -> int:
    print(f"  status: muhitlar={args.muhit or ['hammasi']}, format={args.format}")
    return 0


def parser_yarat() -> argparse.ArgumentParser:
    umumiy = argparse.ArgumentParser(add_help=False)
    umumiy.add_argument("-v", "--verbose", action="count", default=0)
    umumiy.add_argument("--quruq", action="store_true")

    asosiy = argparse.ArgumentParser(prog="relizchi", description="Relizlarni boshqarish", color=False)
    asosiy.add_argument("--version", action="version", version="%(prog)s 2.4.0")
    buyruqlar = asosiy.add_subparsers(dest="buyruq", required=True, metavar="BUYRUQ")

    d = buyruqlar.add_parser("deploy", parents=[umumiy], help="yangi versiyani joylashtirish", color=False)
    d.add_argument("muhit", choices=["staging", "prod"])
    d.add_argument("versiya")
    d.set_defaults(ishlovchi=deploy)

    r = buyruqlar.add_parser("rollback", parents=[umumiy], help="oldingi versiyaga qaytish", color=False)
    r.add_argument("muhit", choices=["staging", "prod"])
    r.add_argument("--qadam", type=int, default=1)
    r.set_defaults(ishlovchi=qaytar)

    s = buyruqlar.add_parser("status", parents=[umumiy], help="holatni ko'rsatish", color=False)
    s.add_argument("muhit", nargs="*")
    s.add_argument("--format", choices=["jadval", "json"], default="jadval")
    s.set_defaults(ishlovchi=holat)
    return asosiy


def main(argv: list[str] | None = None) -> int:
    args = parser_yarat().parse_args(argv)
    return args.ishlovchi(args)


def ishga_tushir(argv: list[str]) -> None:
    xato, chiqish = io.StringIO(), io.StringIO()
    with contextlib.redirect_stderr(xato), contextlib.redirect_stdout(chiqish):
        try:
            kod = main(argv)
        except SystemExit as s:
            kod = s.code
    print(f"$ relizchi {' '.join(argv)}")
    for qator in chiqish.getvalue().splitlines()[:12]:
        print(qator if qator.startswith("  ") else f"  {qator}")
    if xato.getvalue():
        print(f"  [stderr] {xato.getvalue().strip().splitlines()[-1]}")
    print(f"  → chiqish kodi {kod}\n")


def main_misol() -> None:
    for argv in (["deploy", "prod", "2.4.0", "-vv"], ["rollback", "prod", "--qadam", "5"], ["status", "staging", "prod", "--format", "json"],
                 [], ["deploy", "test", "2.4.0"], ["--version"], ["-h"]):
        ishga_tushir(argv)


if __name__ == "__main__":
    main_misol()

Natijaning muhim qismi:

text
$ relizchi deploy prod 2.4.0 -vv
  deploy: muhit=prod, versiya=2.4.0, quruq=False, verbose=2
  → chiqish kodi 0

$ relizchi rollback prod --qadam 5
  [stderr] xato: 5 qadam orqaga — ko'pi bilan 3
  → chiqish kodi 1

$ relizchi status staging prod --format json
  status: muhitlar=['staging', 'prod'], format=json
  → chiqish kodi 0

$ relizchi
  [stderr] relizchi: error: the following arguments are required: BUYRUQ
  → chiqish kodi 2

$ relizchi deploy test 2.4.0
  [stderr] relizchi deploy: error: argument muhit: invalid choice: 'test' (choose from 'staging', 'prod')
  → chiqish kodi 2

$ relizchi --version
  relizchi 2.4.0
  → chiqish kodi 0

$ relizchi -h
  usage: relizchi [-h] [--version] BUYRUQ ...

  Relizlarni boshqarish

  positional arguments:
  BUYRUQ
    deploy    yangi versiyani joylashtirish
    rollback  oldingi versiyaga qaytish
    status    holatni ko'rsatish

  options:
  -h, --help  show this help message and exit
  → chiqish kodi 0

Nima ko'rsatdi: 2.5, 2.6, 2.7-bo'limlar.

Misol 4 — Amaliy: vazifalar menejeri CLI

To'liq CLI dastur: vazifa qosh, vazifa royxat, vazifa bajar, vazifa ochir. Ma'lumotlar JSON faylda (joylashuvi --fayl yoki VAZIFA_FAYL muhit o'zgaruvchisidan), --json bayrog'i mashina uchun chiqish beradi, xatolar stderr ga va aniq chiqish kodlari bilan. Butun ish main(argv, muhit) orqali — shuning uchun misol butun "sessiyani" jarayon ishga tushirmasdan sinab ko'radi.

python
"""Ichki buyruqlar; muhitdan sukut; JSON saqlash va atomar yozish; ustuvorlik va muddat tekshiruvi; --json chiqish; FoydalanuvchiXatosi va chiqish kodlari; main(argv) sessiyasi."""

import argparse
import contextlib
import io
import json
import os
import sys
import tempfile
from datetime import date
from pathlib import Path


class FoydalanuvchiXatosi(Exception):
    pass


def ustuvorlik(qiymat: str) -> int:
    nomlar = {"past": 1, "orta": 2, "yuqori": 3}
    if qiymat not in nomlar:
        raise argparse.ArgumentTypeError(f"ustuvorlik {sorted(nomlar)} dan biri bo'lishi kerak")
    return nomlar[qiymat]


def yukla(fayl: Path) -> list[dict]:
    return json.loads(fayl.read_text(encoding="utf-8")) if fayl.exists() else []


def saqla(fayl: Path, vazifalar: list[dict]) -> None:
    vaqtinchalik = fayl.with_suffix(".tmp")
    vaqtinchalik.write_text(json.dumps(vazifalar, ensure_ascii=False, indent=1), encoding="utf-8")
    os.replace(vaqtinchalik, fayl)


def topish(vazifalar: list[dict], id_: int) -> dict:
    for v in vazifalar:
        if v["id"] == id_:
            return v
    raise FoydalanuvchiXatosi(f"{id_}-vazifa topilmadi")


def qosh(args: argparse.Namespace) -> int:
    vazifalar = yukla(args.fayl)
    if args.muddat and args.muddat < args.bugun:
        raise FoydalanuvchiXatosi(f"muddat o'tmishda: {args.muddat}")
    yangi = {"id": max((v["id"] for v in vazifalar), default=0) + 1, "matn": args.matn,
             "ustuvorlik": args.ustuvorlik, "muddat": args.muddat.isoformat() if args.muddat else None, "bajarildi": False}
    vazifalar.append(yangi)
    saqla(args.fayl, vazifalar)
    print(json.dumps(yangi, ensure_ascii=False) if args.json else f"qo'shildi: #{yangi['id']} {yangi['matn']}")
    return 0


def royxat(args: argparse.Namespace) -> int:
    vazifalar = [v for v in yukla(args.fayl) if args.hammasi or not v["bajarildi"]]
    vazifalar.sort(key=lambda v: (-v["ustuvorlik"], v["muddat"] or "9999", v["id"]))
    if args.json:
        print(json.dumps(vazifalar, ensure_ascii=False))
        return 0
    if not vazifalar:
        print("vazifalar yo'q")
    belgi = {1: "·", 2: "!", 3: "‼"}
    for v in vazifalar:
        holat = "✔" if v["bajarildi"] else " "
        muddat = f" (muddat: {v['muddat']})" if v["muddat"] else ""
        print(f"[{holat}] #{v['id']} {belgi[v['ustuvorlik']]} {v['matn']}{muddat}")
    return 0


def bajar(args: argparse.Namespace) -> int:
    vazifalar = yukla(args.fayl)
    for id_ in args.idlar:
        topish(vazifalar, id_)["bajarildi"] = True
    saqla(args.fayl, vazifalar)
    print(f"bajarildi: {', '.join(f'#{i}' for i in args.idlar)}")
    return 0


def ochir(args: argparse.Namespace) -> int:
    vazifalar = yukla(args.fayl)
    topish(vazifalar, args.id)
    if not args.tasdiq:
        raise FoydalanuvchiXatosi("o'chirish uchun --tasdiq bayrog'i kerak")
    saqla(args.fayl, [v for v in vazifalar if v["id"] != args.id])
    print(f"o'chirildi: #{args.id}")
    return 0


def parser_yarat(muhit: dict[str, str]) -> argparse.ArgumentParser:
    umumiy = argparse.ArgumentParser(add_help=False)
    umumiy.add_argument("--fayl", type=Path, default=Path(muhit.get("VAZIFA_FAYL", "vazifalar.json")))
    umumiy.add_argument("--json", action="store_true", help="mashina o'qiydigan chiqish")

    p = argparse.ArgumentParser(prog="vazifa", color=False)
    b = p.add_subparsers(dest="buyruq", required=True)
    q = b.add_parser("qosh", parents=[umumiy], color=False)
    q.add_argument("matn")
    q.add_argument("-u", "--ustuvorlik", type=ustuvorlik, default="orta")
    q.add_argument("--muddat", type=date.fromisoformat)
    q.set_defaults(ishlovchi=qosh)
    r = b.add_parser("royxat", parents=[umumiy], color=False)
    r.add_argument("-a", "--hammasi", action="store_true")
    r.set_defaults(ishlovchi=royxat)
    bj = b.add_parser("bajar", parents=[umumiy], color=False)
    bj.add_argument("idlar", type=int, nargs="+")
    bj.set_defaults(ishlovchi=bajar)
    o = b.add_parser("ochir", parents=[umumiy], color=False)
    o.add_argument("id", type=int)
    o.add_argument("--tasdiq", action="store_true")
    o.set_defaults(ishlovchi=ochir)
    return p


def main(argv: list[str] | None = None, muhit: dict[str, str] | None = None, bugun: date | None = None) -> int:
    muhit = dict(os.environ) if muhit is None else muhit
    args = parser_yarat(muhit).parse_args(argv)
    args.bugun = bugun or date.today()
    try:
        return args.ishlovchi(args)
    except FoydalanuvchiXatosi as xato:
        print(f"xato: {xato}", file=sys.stderr)
        return 1


def main_misol() -> None:
    with tempfile.TemporaryDirectory() as katalog:
        muhit = {"VAZIFA_FAYL": str(Path(katalog) / "vazifalar.json")}
        sessiya = [
            ["qosh", "15.15-darsni yozish", "-u", "yuqori", "--muddat", "2026-09-18"],
            ["qosh", "Zaxirani tekshirish", "-u", "past"],
            ["qosh", "Serverni yangilash", "--muddat", "2026-09-20"],
            ["qosh", "Eski vazifa", "--muddat", "2026-09-01"],
            ["qosh", "Noto'g'ri", "-u", "shoshilinch"],
            ["royxat"],
            ["bajar", "1", "3"],
            ["bajar", "9"],
            ["royxat", "--hammasi"],
            ["ochir", "2"],
            ["ochir", "2", "--tasdiq"],
            ["royxat", "--hammasi", "--json"],
        ]
        for argv in sessiya:
            chiqish, xato = io.StringIO(), io.StringIO()
            with contextlib.redirect_stdout(chiqish), contextlib.redirect_stderr(xato):
                try:
                    kod = main(argv, muhit, bugun=date(2026, 9, 17))
                except SystemExit as s:
                    kod = s.code
            print(f"$ vazifa {' '.join(argv)}")
            for qator in chiqish.getvalue().splitlines():
                print(f"  {qator}")
            if xato.getvalue():
                print(f"  [stderr] {xato.getvalue().strip().splitlines()[-1]}")
            print(f"  (kod {kod})")
        saqlangan = json.loads((Path(katalog) / "vazifalar.json").read_text(encoding="utf-8"))
        print(f"\nFaylda {len(saqlangan)} ta vazifa, .tmp qolmadi: {not (Path(katalog) / 'vazifalar.tmp').exists()}")


if __name__ == "__main__":
    main_misol()

Natijaning muhim qismi:

text
$ vazifa qosh 15.15-darsni yozish -u yuqori --muddat 2026-09-18
  qo'shildi: #1 15.15-darsni yozish
  (kod 0)
$ vazifa qosh Zaxirani tekshirish -u past
  qo'shildi: #2 Zaxirani tekshirish
  (kod 0)
$ vazifa qosh Serverni yangilash --muddat 2026-09-20
  qo'shildi: #3 Serverni yangilash
  (kod 0)
$ vazifa qosh Eski vazifa --muddat 2026-09-01
  [stderr] xato: muddat o'tmishda: 2026-09-01
  (kod 1)
$ vazifa qosh Noto'g'ri -u shoshilinch
  [stderr] vazifa qosh: error: argument -u/--ustuvorlik: ustuvorlik ['orta', 'past', 'yuqori'] dan biri bo'lishi kerak
  (kod 2)
$ vazifa royxat
  [ ] #1 ‼ 15.15-darsni yozish (muddat: 2026-09-18)
  [ ] #3 ! Serverni yangilash (muddat: 2026-09-20)
  [ ] #2 · Zaxirani tekshirish
  (kod 0)
$ vazifa bajar 1 3
  bajarildi: #1, #3
  (kod 0)
$ vazifa bajar 9
  [stderr] xato: 9-vazifa topilmadi
  (kod 1)
$ vazifa royxat --hammasi
  [✔] #1 ‼ 15.15-darsni yozish (muddat: 2026-09-18)
  [✔] #3 ! Serverni yangilash (muddat: 2026-09-20)
  [ ] #2 · Zaxirani tekshirish
  (kod 0)
$ vazifa ochir 2
  [stderr] xato: o'chirish uchun --tasdiq bayrog'i kerak
  (kod 1)
$ vazifa ochir 2 --tasdiq
  o'chirildi: #2
  (kod 0)
$ vazifa royxat --hammasi --json
  [{"id": 1, "matn": "15.15-darsni yozish", "ustuvorlik": 3, "muddat": "2026-09-18", "bajarildi": true}, {"id": 3, "matn": "Serverni yangilash", "ustuvorlik": 2, "muddat": "2026-09-20", "bajarildi": true}]
  (kod 0)

Faylda 2 ta vazifa, .tmp qolmadi: True

Nima ko'rsatdi: 2.1–2.7-bo'limlar.


5. To'g'ri va noto'g'ri tushunishlar

Noto'g'ri fikr To'g'risi
"type=bool mantiqiy bayroq" bool("False") — True; store_true yoki BooleanOptionalAction
"Sukut qiymat type dan o'tmaydi" Satr sukut o'tadi
"argparse xatoda istisno beradi" SystemExit(2); exit_on_error=False bilan ArgumentError
"Noma'lum argument jim e'tiborsiz qoladi" Xato — parse_known_args bundan mustasno
"Ichki buyruqsiz ishga tushirish xato beradi" Faqat required=True bilan
"argparse argumentlararo shartlarni tekshiradi" Yo'q — parser.error bilan o'zingiz
"suggest_on_error bayroq nomidagi xatoni tuzatadi" 3.14 da choices va buyruqlar uchun taklif beradi
"parse_args() ni testlash uchun jarayon kerak" parse_args(ro'yxat) va main(argv)

6. Keng tarqalgan xatolar va yechimlari

1. type=bool

python
p.add_argument("--tozala", type=bool)                     # ❌
p.add_argument("--tozala", action="store_true")           # ✅

2. Qo'lda sys.argv

python
if "--quruq" in sys.argv: ...                             # ❌ noma'lum argumentlar jim
args = p.parse_args()                                     # ✅

3. Testlanmaydigan tuzilma

python
args = p.parse_args(); ... sys.exit(...)                  # ❌ modul darajasida
def main(argv=None) -> int: ...                           # ✅

4. if/elif bilan buyruqlar

python
if args.buyruq == "qosh": ... elif ...                    # ⚠️
buyruq.set_defaults(ishlovchi=qosh); args.ishlovchi(args) # ✅

5. Argumentlararo shart tekshirilmagan

python
args = p.parse_args()                                     # ⚠️ --oxiri < --boshi
if args.oxiri < args.boshi: p.error("...")                # ✅

6. prog berilmagan

python
argparse.ArgumentParser()                                 # ⚠️ usage da skript fayl nomi
argparse.ArgumentParser(prog="vazifa")                    # ✅

7. Foydalanuvchi xatosi va traceback

python
return args.ishlovchi(args)                               # ⚠️ KeyError traceback
except FoydalanuvchiXatosi as x: print(x, file=sys.stderr); return 1   # ✅

8. append va bo'sh bo'lmagan sukut

python
p.add_argument("--teg", action="append", default=["umumiy"])   # ⚠️ ["umumiy", "yangi"]

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 15.11-dars (o'tilgan): sys.argv, chiqish kodlari, stdout/stderr
  • 15.14-dars (o'tilgan): CLI dasturlarni subprocess bilan chaqirish
  • 15.16-dars: logging — -v darajasini jurnal darajasiga bog'lash
  • 17-qism: CLI ni main(argv) orqali testlash, capsys
  • 19.6-dars: CLI dasturni paket sifatida chiqarish — project.scripts, click, typer
  • 26-qism: avtomatlashtirish vositalari

8. Eng yaxshi amaliyotlar

  1. main(argv: list[str] | None = None) -> int va sys.exit(main()).

  2. Parserni alohida funksiyada yarating.

  3. Mantiqiy bayroqlar — store_true yoki BooleanOptionalAction.

  4. Qiymat tekshiruvi — type, choices, o'z tur funksiyasi.

  5. Argumentlararo shartlar — parser.error.

  6. Buyruqlar — add_subparsers(required=True) va set_defaults(ishlovchi=...).

  7. Har argumentga help, sukut uchun %(default)s.

  8. Mashina uchun --json chiqish, odam uchun jadval.


9. Amaliy topshiriq

Vazifa 1: Natijani bashorat qiling

python
import argparse
p = argparse.ArgumentParser(prog="x")
p.add_argument("fayl")
p.add_argument("-n", type=int, default="3")
p.add_argument("-v", action="count", default=0)
p.add_argument("--teg", action="append", default=[])
p.add_argument("--rang", action=argparse.BooleanOptionalAction, default=True)
1.  print(p.parse_args(["a"]).n)
2.  print(type(p.parse_args(["a"]).n).__name__)
3.  print(p.parse_args(["a", "-vvv"]).v)
4.  print(p.parse_args(["a", "--teg", "x", "--teg", "y"]).teg)
5.  print(p.parse_args(["a", "--no-rang"]).rang)
6.  print(p.parse_args(["-n7", "a"]).n)
7.  print(p.parse_args(["--", "-a"]).fayl)
8.  print(vars(p.parse_args(["a"]))["fayl"])
9.  q = argparse.ArgumentParser(); q.add_argument("--d", type=bool)
    print(q.parse_args(["--d", ""]).d)
10. print(p.parse_known_args(["a", "--z"])[1])
11. try:
        p.parse_args([])
    except SystemExit as e:
        print(e.code)
12. print(bool("False"))
Javoblar
  1. 3
  2. int
  3. 3
  4. ['x', 'y']
  5. False
  6. 7
  7. -a
  8. a
  9. False — faqat bo'sh satr False
  10. ['--z']
  11. 2 (va stderr ga xato xabari)
  12. True

Vazifa 2: Xatolarni tuzating

python
1.  p.add_argument("--quruq", type=bool, default=False)

2.  args = p.parse_args()
    if args.buyruq == "qosh": qosh(args)
    elif args.buyruq == "ochir": ochir(args)
    # buyruq berilmasa hech narsa bo'lmaydi

3.  import sys
    fayl = sys.argv[1]
    soni = int(sys.argv[2]) if len(sys.argv) > 2 else 10

4.  p.add_argument("--boshi"); p.add_argument("--oxiri")
    args = p.parse_args()
    hisobot(args.boshi, args.oxiri)          # "2026-09-30" > "2026-10-01" solishtirilmaydi

5.  args = argparse.ArgumentParser().parse_args()   # modul darajasida
    sys.exit(ishla(args))
Javoblar
python
1.  p.add_argument("--quruq", action="store_true")

2.  b = p.add_subparsers(dest="buyruq", required=True)
    b.add_parser("qosh").set_defaults(ishlovchi=qosh)
    b.add_parser("ochir").set_defaults(ishlovchi=ochir)
    args = p.parse_args()
    sys.exit(args.ishlovchi(args))

3.  p = argparse.ArgumentParser(prog="dastur")
    p.add_argument("fayl", type=Path)
    p.add_argument("soni", type=int, nargs="?", default=10)
    args = p.parse_args()

4.  p.add_argument("--boshi", type=date.fromisoformat, required=True)
    p.add_argument("--oxiri", type=date.fromisoformat, required=True)
    args = p.parse_args()
    if args.oxiri < args.boshi:
        p.error("--oxiri --boshi dan oldin bo'lishi mumkin emas")

5.  def main(argv=None) -> int:
        args = parser_yarat().parse_args(argv)
        return ishla(args)
    if __name__ == "__main__":
        sys.exit(main())

Vazifa 3: mini-grep

mini_grep NAQSH [FAYL ...] — argparse va re (15.1–15.3) bilan:

  1. -i (harfga farqsiz), -n (qator raqami), -c (faqat son), -v (teskari)
  2. Fayllar berilmasa — stdin dan o'qish
  3. -r bilan kataloglarni rekursiv (pathlib)
  4. Topilsa kod 0, topilmasa 1, xato bo'lsa 2 (asl grep kabi)
  5. main(argv) va 10 ta test

Vazifa 4: Konfiguratsiya ustuvorligi

CLI yozing, sozlama qiymati quyidagi tartibda aniqlansin: CLI argument > muhit o'zgaruvchisi > TOML fayl > sukut:

  1. --port, --rejim, --baza-url
  2. DASTUR_PORT va boshqalar
  3. --sozlama config.toml (16.6-darsgacha oddiy KALIT=qiymat fayl bilan)
  4. --korsat — har qiymat va uning manbasini chiqarsin
  5. Barcha kombinatsiyalar uchun testlar

Vazifa 5: Ichki buyruqli git-ga o'xshash vosita

eslatma vositasi: init, yoz, royxat, qidir, teg qosh, teg ochir:

  1. Ichki-ichki buyruqlar (teg qosh)
  2. Umumiy --katalog va -v bayroqlari (parents)
  3. Har buyruqda --help misollar bilan (epilog, RawDescriptionHelpFormatter)
  4. --json chiqish

Vazifa 6: Yordam matnidan hujjat

  1. Misol 4 dagi parserdan barcha buyruqlar va argumentlarni aylanib chiqing (_subparsers, _actions — ichki API ekanini hujjatlashtiring)
  2. Markdown hujjat yarating: har buyruq uchun jadval (argument, turi, sukut, tavsif)
  3. Hujjat va kod mos ekanini testda tekshiring

Vazifa 7: O'ylash

Buyruq qatori interfeyslari (CLI) 50 yildan ortiq mavjud va grafik interfeyslar, veb va mobil ilovalarga qaramay hali ham dasturchilar va tizim administratorlarining asosiy vositasi. Nega? Yaxshi CLI dizaynining qanday tamoyillari bor va ular API dizayniga qanday o'xshaydi?

Javob

Qisqa javob: CLI birlashuvchan (composable), avtomatlashtiriladigan va matnli — bir dasturning chiqishi boshqasining kirishi bo'ladi, har qanday buyruq skriptga, CI ga va cron ga aylanadi. Yaxshi CLI — bu inson va mashina uchun API: barqaror shartnoma (argumentlar, chiqish formati, chiqish kodlari), bashorat qilinadigan xulq va aniq xatolar.

1. Nega CLI yashab qolgan

Xususiyat Nima beradi
Birlashuvchanlik `git log
Avtomatlashtirish Har buyruq — skript, CI, cron
Takrorlanuvchanlik Buyruq tarixini saqlash va qayta bajarish
Masofadan ishlash SSH orqali, grafik interfeyssiz
Resurs Sekin tarmoq va zaif serverda ham
Aniqlik "Bu tugmani bosing" emas, aniq buyruq

2. Unix falsafasi

  1. Bitta ishni yaxshi qiling
  2. Chiqish — boshqa dasturning kirishi (ortiqcha bezaksiz)
  3. Matn — universal interfeys

3. Yaxshi CLI tamoyillari

Tamoyil Amaliyot
Bashorat qilinadigan nomlar --help, --version, -v, -q, --dry-run
Xavfsiz sukut Xavfli amal — --force/--tasdiq bilan
Chiqish kodlari 0 / 1 / 2 (15.11)
stdout — ma'lumot, stderr — diagnostika Quvurlar buzilmaydi
Mashina formati --json, --porcelain (git)
Aniq xatolar Nima noto'g'ri va qanday tuzatish — maybe you meant ...
Orqaga moslik Bayroqlarni o'zgartirish — buzuvchi o'zgarish
Tezlik Ishga tushish vaqti sezilmasin
Interaktivlik ixtiyoriy TTY bo'lmasa savol bermaslik

4. CLI va API o'xshashligi

API CLI
Endpoint Buyruq / ichki buyruq
Parametrlar va turlar Argumentlar, type, choices
HTTP status Chiqish kodi
JSON javob --json chiqish
Hujjat (OpenAPI) --help, man sahifa
Versiyalash --version, eskirish ogohlantirishlari
Validatsiya xatosi 400/422 Kod 2, usage

git --porcelain — mashhur misol: odam o'qiydigan chiqish o'zgarishi mumkin, --porcelain formati esa barqaror shartnoma.

5. Xulosa

  1. CLI — birlashuvchan va avtomatlashtiriladigan interfeys, shuning uchun eskirmaydi
  2. Yaxshi CLI — inson va mashina uchun API
  3. Argumentlar, chiqish formati va kodlari — ommaviy shartnoma, ularni o'zgartirish ehtiyotkorlik talab qiladi
  4. argparse bu tamoyillarning ko'pini standart holda beradi: yordam, xatolar, kod 2

Nimani mustahkamlaydi: 2.1–2.7-bo'limlar.


Xulosa

Bu darsda buyruq qatori dasturlarini argparse bilan yaratishni o'rgandik.

Eng muhim uch fikr:

  1. Argumentlar va amallar — deklarativ. Pozitsion va ixtiyoriy argumentlar type, default, choices bilan e'lon qilinadi; -n 5, -n5, --soni=5 bir xil ishlaydi, satr sukut ham type dan o'tadi. Amallar: store_true, count (-vvv), append, nargs, BooleanOptionalAction (--rang/--no-rang). type=bool — tuzoq: --tozala False jadvalni tozalaydi.

  2. Xatolar standart va aniq. Noto'g'ri argument — stderr ga usage va xabar, chiqish kodi 2; o'z tur funksiyasi ArgumentTypeError bilan, argumentlararo shartlar esa parse_args dan keyin parser.error bilan tekshiriladi. exit_on_error=False chiqish o'rniga ArgumentError beradi, 3.14 ning suggest_on_error esa choices da maybe you meant 'json'? deb taklif qiladi.

  3. Tuzilma: ichki buyruqlar va main(argv) -> int. add_subparsers(required=True), har buyruqqa set_defaults(ishlovchi=...), umumiy bayroqlar parents bilan. main(argv) butun CLI ni jarayon ishga tushirmasdan testlash imkonini beradi — vazifalar menejeri misolida butun sessiya (qo'shish, xato, bajarish, o'chirish, JSON chiqish) chiqish kodlari bilan tekshirildi.

Keyingi darsda 15-qismni logging bilan yakunlaymiz: jurnal darajalari, handlerlar, formatlash, konfiguratsiya va nega print emas.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
15.15-dars: argparse — CLI — IlmHamroh