Sepanjang section ini kita membangun CLI sedikit demi sedikit: argparse untuk argumen dan subperintah, environment variables untuk konfigurasi, exit code untuk kontrak dengan pemanggil, dan pathlib untuk pekerjaan berkas. Chapter penutup ini menyatukan semuanya menjadi satu program nyata — pengatur berkas otomatis bernama organizer.py — lalu mempercantik keluarannya dengan rich dan menguji perilakunya dengan pytest.

Merancang organizer #

Program kita merapikan sebuah direktori: setiap berkas dipindahkan ke subfolder sesuai kategorinya (.txt dan .pdf ke dokumen, .png ke gambar, .csv ke data). Pengaturan teknis datang dari tiga lapis: flag --apply dari CLI, variabel ORGANIZER_TARGET dari environment, dan pemetaan ekstensi sebagai default dalam kode. Berkas lengkapnya:

python
import argparse
import logging
import os
import sys
from pathlib import Path

KATEGORI = {
    ".png": "gambar",
    ".jpg": "gambar",
    ".txt": "dokumen",
    ".pdf": "dokumen",
    ".csv": "data",
}


def buat_parser():
    parser = argparse.ArgumentParser(
        prog="organizer.py",
        description="Merapikan berkas ke folder berdasarkan kategori.",
    )
    parser.add_argument("direktori", help="direktori yang akan dirapikan")
    parser.add_argument("--apply", action="store_true",
                        help="benar-benar memindahkan berkas (default: dry-run)")
    parser.add_argument("--verbose", action="store_true",
                        help="tampilkan log level DEBUG")
    return parser


def main(argv=None):
    args = buat_parser().parse_args(argv)
    logging.basicConfig(
        level=logging.DEBUG if args.verbose else logging.INFO,
        format="%(levelname)-7s %(message)s",
    )

    sumber = Path(args.direktori)
    if not sumber.is_dir():
        logging.error("direktori tidak ditemukan: %s", args.direktori)
        return 1

    target = Path(os.environ.get("ORGANIZER_TARGET", sumber))
    jumlah = 0
    for berkas in sorted(sumber.iterdir()):
        if not berkas.is_file() or berkas.suffix.lower() not in KATEGORI:
            logging.debug("dilewati: %s", berkas.name)
            continue
        tujuan = target / KATEGORI[berkas.suffix.lower()] / berkas.name
        if args.apply:
            tujuan.parent.mkdir(parents=True, exist_ok=True)
            berkas.replace(tujuan)
        logging.info("%s -> %s", berkas.name, tujuan.as_posix())
        jumlah += 1

    logging.info("total berkas: %d (apply=%s)", jumlah, args.apply)
    return 0


if __name__ == "__main__":
    sys.exit(main())

Beberapa keputusan desain yang patut dicermati:

  1. Baris 31 (logging.basicConfig(): level log dikendalikan flag --verbose; format tanpa asctime menjaga baris log singkat karena pencatatan waktu biasanya disediakan penjadwal.
  2. Baris 37-39 (if not sumber.is_dir()): input tidak valid mengembalikan 1, bukan traceback — pengguna mendapat pesan yang jelas.
  3. Baris 41 (os.environ.get("ORGANIZER_TARGET", sumber)): environment variable boleh mengalihkan folder tujuan tanpa mengubah argumen; urutan prioritas mengikuti konvensi Chapter Environment Variables.
  4. Baris 48-50 (if args.apply:): tanpa flag tersebut program berjalan dry-run — kebiasaan dari Chapter Filesystem Automation yang membuat operasi berkas bisa diaudit dulu.

Menjalankan organizer #

Siapkan folder berkas-kerja berisi catatan.txt, foto-liburan.png, penjualan.csv, laporan.pdf, dan lagu.mp3. Eksekusi pertama tanpa --apply hanya menampilkan rencana:

bash
python3 organizer.py berkas-kerja

Output yang Dihasilkan (log dikirim ke stderr):

text
INFO    catatan.txt -> berkas-kerja/dokumen/catatan.txt
INFO    foto-liburan.png -> berkas-kerja/gambar/foto-liburan.png
INFO    laporan.pdf -> berkas-kerja/dokumen/laporan.pdf
INFO    penjualan.csv -> berkas-kerja/data/penjualan.csv
INFO    total berkas: 4 (apply=False)

Tambahkan --apply untuk menjalankan sungguhan, lalu tampilkan struktur hasilnya:

bash
python3 organizer.py berkas-kerja --apply
python3 -c "from pathlib import Path; [print(p.as_posix()) for p in sorted(Path('berkas-kerja').rglob('*'))]"

Output yang Dihasilkan:

text
INFO    catatan.txt -> berkas-kerja/dokumen/catatan.txt
INFO    foto-liburan.png -> berkas-kerja/gambar/foto-liburan.png
INFO    laporan.pdf -> berkas-kerja/dokumen/laporan.pdf
INFO    penjualan.csv -> berkas-kerja/data/penjualan.csv
INFO    total berkas: 4 (apply=True)
berkas-kerja/data
berkas-kerja/data/penjualan.csv
berkas-kerja/dokumen
berkas-kerja/dokumen/catatan.txt
berkas-kerja/dokumen/laporan.pdf
berkas-kerja/gambar
berkas-kerja/gambar/foto-liburan.png
berkas-kerja/lagu.mp3

Berkas lagu.mp3 tetap di tempatnya karena .mp3 tidak terdaftar di KATEGORI; jalankan ulang dengan --verbose dan berkas tersebut muncul sebagai DEBUG dilewati: lagu.mp3. Directori yang salah mengembalikan exit code 1:

bash
python3 organizer.py folder-tidak-ada
echo $?

Output yang Dihasilkan:

text
ERROR   direktori tidak ditemukan: folder-tidak-ada
1

Tampilan dengan rich #

Untuk CLI yang dipakai manusia setiap hari, tabel dan panel dari library rich membuat ringkasan jauh lebih mudah dipindai. Berkas tabel_rich.py:

python
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

console = Console()

tabel = Table(title="Hasil Organisasi")
tabel.add_column("Kategori", style="cyan")
tabel.add_column("Berkas", style="green")
tabel.add_column("Status", justify="center")

for kategori, nama, status in (
    ("dokumen", "catatan.txt", "pindah"),
    ("gambar", "foto-liburan.png", "pindah"),
    ("data", "penjualan.csv", "pindah"),
    ("-", "lagu.mp3", "dilewati"),
):
    tabel.add_row(kategori, nama, status)

console.print(tabel)
console.print(Panel("4 berkas dipindahkan, 1 dilewati", title="Ringkasan"))

Output yang Dihasilkan:

text
             Hasil Organisasi             
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Kategori ┃ Berkas           ┃  Status  ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ dokumen  │ catatan.txt      │  pindah  │
│ gambar   │ foto-liburan.png │  pindah  │
│ data     │ penjualan.csv    │  pindah  │
│ -        │ lagu.mp3         │ dilewati │
└──────────┴──────────────────┴──────────┘
╭───────────────────────────────── Ringkasan ──────────────────────────────────╮
│ 4 berkas dipindahkan, 1 dilewati                                             │
╰──────────────────────────────────────────────────────────────────────────────╯

Untuk pekerjaan yang memakan waktu, rich.progress.Progress menampilkan bar kemajuan yang terisi secara langsung di terminal — satu baris progress.advance(tugas) di dalam loop sudah cukup. Indikasi kemajuan hanya untuk kerja yang benar-benar lama; bar yang selesai dalam satu detik hanya menambah kebisingan.

Menguji CLI dengan pytest #

Struktur buat_parser() dan main(argv=None) terbayar di sini: test memanggil main() dengan list argumen, tanpa menjalankan proses baru. Fixture tmp_path menyediakan folder sementara, caplog menangkap keluaran logging, dan monkeypatch mengatur environment variable lalu memulihkannya otomatis. Berkas test_organizer.py:

python
import logging

import organizer


def test_dry_run_tidak_memindah(tmp_path, caplog):
    caplog.set_level(logging.INFO)

    (tmp_path / "catatan.txt").write_text("isi catatan")

    kode_keluar = organizer.main([str(tmp_path)])

    assert kode_keluar == 0
    assert (tmp_path / "catatan.txt").exists()
    assert "catatan.txt" in caplog.text


def test_apply_dengan_env_target(tmp_path, monkeypatch):
    sumber = tmp_path / "sumber"
    sumber.mkdir()
    (sumber / "penjualan.csv").write_text("produk,harga")
    monkeypatch.setenv("ORGANIZER_TARGET", str(tmp_path / "target"))

    kode_keluar = organizer.main([str(sumber), "--apply"])

    assert kode_keluar == 0
    assert (tmp_path / "target" / "data" / "penjualan.csv").exists()

Jalankan dengan python3 -m pytest test_organizer.py -q:

text
..
2 passed in 0.13s

Test pertama membuktikan dry-run tidak memindahkan berkas; test kedua membuktikan --apply menghormati ORGANIZER_TARGET. Untuk CLI yang mencetak lewat print(), fixture capsys menggantikan caplog — keduanya adalah fixture bawaan pytest yang dibahas tuntas pada Chapter Menulis Test dengan pytest dan Mocking dan Patching. Angka durasi pada baris terakhir akan berbeda di komputer Anda.

Entry Point dan Checklist UX #

Agar program dipanggil sebagai organizer alih-alih python3 organizer.py, deklarasikan entry point pada pyproject.toml:

toml
[project.scripts]
organizer = "organizer:main"

Setelah paket dipasang, pip membuat pembungkus kecil yang memanggil main() — mekanisme lengkapnya dibahas pada Chapter Membuat Command-Line Entry Points di section Packaging dan Deployment. Sebelum merilis, telusuri checklist pengalaman pengguna berikut:

  • --help yang rapi: deskripsi jelas, contoh pemakaian, nilai default disebut eksplisit.
  • dry-run untuk setiap operasi yang mengubah data, plus --verbose untuk pengguna yang menggali.
  • exit code konsisten: 0 sukses, 1 galat aplikasi, 2 kesalahan penggunaan.
  • log ke stderr dan hasil ke stdout, sehingga keduanya bisa dialihkan terpisah.
  • pesan galat yang menyebut solusi, bukan hanya traceback.
  • program tetap berjalan tanpa paket opsional seperti rich.

Dengan struktur, kontrak, tampilan, dan test di tempatnya, organizer.py bukan lagi skrip — ia adalah alat yang siap dipercaya menjaga direktori Anda setiap hari.