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:
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:
- Baris 31 (
logging.basicConfig(): level log dikendalikan flag--verbose; format tanpaasctimemenjaga baris log singkat karena pencatatan waktu biasanya disediakan penjadwal. - Baris 37-39 (
if not sumber.is_dir()): input tidak valid mengembalikan1, bukan traceback — pengguna mendapat pesan yang jelas. - Baris 41 (
os.environ.get("ORGANIZER_TARGET", sumber)): environment variable boleh mengalihkan folder tujuan tanpa mengubah argumen; urutan prioritas mengikuti konvensi Chapter Environment Variables. - 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:
python3 organizer.py berkas-kerjaOutput yang Dihasilkan (log dikirim ke stderr):
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:
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:
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.mp3Berkas 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:
python3 organizer.py folder-tidak-ada
echo $?Output yang Dihasilkan:
ERROR direktori tidak ditemukan: folder-tidak-ada
1Tampilan 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:
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:
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:
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:
..
2 passed in 0.13sTest 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:
[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:
--helpyang rapi: deskripsi jelas, contoh pemakaian, nilai default disebut eksplisit.- dry-run untuk setiap operasi yang mengubah data, plus
--verboseuntuk pengguna yang menggali. - exit code konsisten:
0sukses,1galat aplikasi,2kesalahan penggunaan. - log ke
stderrdan hasil kestdout, 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.