Bab 19 — Kualitas Kode: Rapi, Tercatat, dan Terdokumentasi

Seri I1 — Kode yang Rapi dan Terawat

CatatanDi bab ini, Anda akan:
  • Mengenal PEP 8, panduan gaya penulisan Python
  • Memakai ruff untuk memeriksa (linter) dan merapikan (formatter) kode secara otomatis
  • Memasang pre-commit supaya pemeriksaan berjalan sendiri sebelum kode disimpan
  • Mencatat jalannya program dengan logging, bukan print
  • Mengenal cara debugging di VS Code dan Jupyter, serta dokumentasi proyek dengan MkDocs dan Sphinx

Semua skrip bab ini ada di folder kode/bab19/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).

Kode lebih sering dibaca daripada ditulis: oleh rekan kerja, dan oleh Anda sendiri enam bulan lagi. Bayangkan buku catatan lapangan. Bila tiap petugas menulis dengan tata letak sendiri-sendiri, mencari satu angka jadi menyiksa. Kode yang seragam dan rapi mudah dibaca, dan kesalahan bodoh lebih cepat ketahuan. Bab 18 menjaga agar kode benar; bab ini menjaga agar kode rapi, tercatat, dan bisa dipahami.

Alur menjaga kualitas sebelum kode disimpan: rapikan, periksa, uji, baru commit.

PEP 8: tata bahasa kode Python

PEP 8 adalah panduan gaya resmi Python (PEP = Python Enhancement Proposal, usulan peningkatan Python). Ringkasannya, dari dokumen resminya (peps.python.org/pep-0008, dibaca 2026-10-10):

Hal Aturan PEP 8
Menjorok 4 spasi per tingkat
Panjang baris maksimal 79 karakter (docstring dan komentar 72)
Nama fungsi dan variabel huruf kecil dengan garis bawah: hitung_biomassa
Nama kelas tiap kata berawalan huruf besar (CapWords): KolamIkan
Konstanta huruf besar semua: BATAS_PH
Impor di atas berkas, satu per baris; kelompokkan: pustaka standar, paket luar, kode sendiri
Spasi satu spasi di sekitar =, ==, and; dua baris kosong antar fungsi dan kelas

Hafalkan prinsipnya, bukan angkanya: konsisten dan mudah dibaca. Untungnya, Anda tidak perlu menghafal. Ada alat yang memeriksanya.

ruff: pemeriksa sekaligus perapi

ruff adalah alat Python (dibuat Astral, pembuat uv) yang menggabungkan dua tugas: linter (pemeriksa yang mencari kesalahan dan kebiasaan buruk) dan formatter (perapi yang menata tampilan). Ia sudah terpasang di environment buku ini; di komputer Anda: python -m pip install ruff. Mari beri makan ruff berkas yang sengaja dibuat berantakan:

01_ruff_periksa.py
# Bab 19 - ruff check: mencari masalah di kode (linter)
from bantu import FOLDER, ruff

contoh = FOLDER / "contoh"
print((contoh / "berantakan.py").read_text(encoding="utf-8"))
print("--- ruff check ---")
teks, kode = ruff("check", "--output-format", "concise", "berantakan.py", folder=contoh)
print(teks)
print("kode keluar:", kode)
import os, sys
import json

def hitung_biomassa(jumlah,bobot_g,riwayat=[]):
    l = jumlah*bobot_g/1000
    tidak_dipakai = 5
    if jumlah == True:
        print( "jumlah aneh" )
    riwayat.append(l)
    print("Biomassa: %s kg" % l)
    return l

class kolam_ikan:
    def __init__(self,nama): self.nama=nama

--- ruff check ---
berantakan.py:1:1: E401 [*] Multiple imports on one line
berantakan.py:1:1: I001 [*] Import block is un-sorted or un-formatted
berantakan.py:1:8: F401 [*] `os` imported but unused
berantakan.py:1:12: F401 [*] `sys` imported but unused
berantakan.py:2:8: F401 [*] `json` imported but unused
berantakan.py:4:44: B006 Do not use mutable data structures for argument defaults
berantakan.py:5:5: E741 Ambiguous variable name: `l`
berantakan.py:6:5: F841 Local variable `tidak_dipakai` is assigned to but never used
berantakan.py:7:8: E712 Avoid equality comparisons to `True`; use `jumlah:` for truth checks
berantakan.py:10:11: UP031 Use format specifiers instead of percent format
berantakan.py:13:7: N801 Class name `kolam_ikan` should use CapWords convention
Found 11 errors.
[*] 5 fixable with `--fix` (4 unsafe fixes available with `--fix --unsafe-fixes`).
kode keluar: 1

Setiap baris laporan berbentuk berkas:baris:kolom: KODE pesan. Beberapa yang muncul:

Kode Arti
E401 beberapa impor dalam satu baris (import os, sys)
I001 urutan impor belum rapi
F401 import yang tidak pernah dipakai
F841 variabel diisi tetapi tidak pernah dipakai
E712 membandingkan dengan True memakai ==
E741 nama yang membingungkan: huruf l mirip angka 1
B006 nilai bawaan parameter berupa list kosong (jebakan: list-nya dipakai bersama oleh semua pemanggilan)
UP031 format teks gaya lama dengan %; gaya baru f-string
N801 nama kelas kolam_ikan seharusnya KolamIkan

Tanda [*] berarti ruff bisa memperbaikinya sendiri. Kode keluar 1 menandakan ada masalah, jadi cocok dipakai di skrip otomatis.

CatatanINGAT

Aturan mana yang diperiksa ditentukan di pyproject.toml. Di proyek kita: select = ["E", "F", "I", "UP", "B", "N"] (kelompok aturan pycodestyle, pyflakes, impor, pembaruan sintaks, kesalahan umum, dan penamaan) dengan line-length = 88. Bawaan ruff memeriksa banyak aturan, dan himpunannya berubah antarversi, jadi menuliskannya sendiri membuat hasil stabil.

ruff format: merapikan tampilan

Sekarang bagian perapi. Opsi --diff memperlihatkan perubahan yang akan dibuat tanpa mengubah berkas:

02_ruff_format_diff.py
# Bab 19 - ruff format --diff: melihat apa yang akan dirapikan, tanpa mengubah berkas
from bantu import FOLDER, ruff

teks, kode = ruff("format", "--diff", "berantakan.py", folder=FOLDER / "contoh")
print(teks)
print("kode keluar:", kode)
--- berantakan.py
+++ berantakan.py
@@ -1,14 +1,17 @@
 import os, sys
 import json
 
-def hitung_biomassa(jumlah,bobot_g,riwayat=[]):
-    l = jumlah*bobot_g/1000
+
+def hitung_biomassa(jumlah, bobot_g, riwayat=[]):
+    l = jumlah * bobot_g / 1000
     tidak_dipakai = 5
     if jumlah == True:
-        print( "jumlah aneh" )
+        print("jumlah aneh")
     riwayat.append(l)
     print("Biomassa: %s kg" % l)
     return l
 
+
 class kolam_ikan:
-    def __init__(self,nama): self.nama=nama
+    def __init__(self, nama):
+        self.nama = nama

1 file would be reformatted
kode keluar: 1

Tanda - adalah baris lama, + baris baru. Perubahannya murni tampilan: spasi setelah koma, dua baris kosong antar fungsi, isi __init__ dipindah ke baris sendiri. Artinya program tidak berubah, hanya cara menulisnya. Kode keluar 1 di sini berarti “ada yang akan diubah”.

ruff check –fix: perbaikan otomatis

03_ruff_perbaiki_otomatis.py
# Bab 19 - ruff check --fix lalu ruff format, pada salinan berkas
import shutil

from bantu import FOLDER, ruff

kerja = FOLDER / "_tmp" / "perbaiki"
shutil.rmtree(kerja, ignore_errors=True)
shutil.copytree(FOLDER / "contoh", kerja)

teks, _ = ruff("check", "--fix", "--output-format", "concise", "berantakan.py", folder=kerja)
print(teks)
print("--- ruff format ---")
teks, _ = ruff("format", "berantakan.py", folder=kerja)
print(teks)
print("--- berkas setelah dirapikan ---")
print((kerja / "berantakan.py").read_text(encoding="utf-8"))
berantakan.py:2:44: B006 Do not use mutable data structures for argument defaults
berantakan.py:3:5: E741 Ambiguous variable name: `l`
berantakan.py:4:5: F841 Local variable `tidak_dipakai` is assigned to but never used
berantakan.py:5:8: E712 Avoid equality comparisons to `True`; use `jumlah:` for truth checks
berantakan.py:8:11: UP031 Use format specifiers instead of percent format
berantakan.py:11:7: N801 Class name `kolam_ikan` should use CapWords convention
Found 10 errors (4 fixed, 6 remaining).
4 unsafe fixes available with `--fix --unsafe-fixes`.
--- ruff format ---
1 file reformatted
--- berkas setelah dirapikan ---
def hitung_biomassa(jumlah, bobot_g, riwayat=[]):
    l = jumlah * bobot_g / 1000
    tidak_dipakai = 5
    if jumlah == True:
        print("jumlah aneh")
    riwayat.append(l)
    print("Biomassa: %s kg" % l)
    return l


class kolam_ikan:
    def __init__(self, nama):
        self.nama = nama

Perhatikan apa yang terjadi. --fix hanya menjalankan perbaikan aman: impor yang tak terpakai dibuang, dan urutan impor dirapikan (pada berkas hasil, baris impor di atas hilang). Enam masalah tersisa, dan ruff mencatat bahwa ada empat perbaikan “tidak aman” (unsafe fixes) yang hanya dijalankan bila Anda memintanya dengan --unsafe-fixes. Alasannya: ubah riwayat=[] atau == True akan mengubah perilaku program, dan keputusan itu ada di tangan Anda, bukan alat. Lalu ruff format merapikan tampilan.

Sisanya diperbaiki dengan tangan. Berikut hasil akhirnya, diperiksa ulang:

04_ruff_hasil_tangan.py
# Bab 19 - Sisa masalah diperbaiki dengan tangan (rapi.py), lalu diperiksa ulang
from bantu import FOLDER, ruff

contoh = FOLDER / "contoh"
print((contoh / "rapi.py").read_text(encoding="utf-8"))
print("--- ruff check ---")
print(ruff("check", "rapi.py", folder=contoh)[0])
print("--- ruff format --check ---")
print(ruff("format", "--check", "rapi.py", folder=contoh)[0])
def hitung_biomassa(jumlah, bobot_g, riwayat=None):
    """Menghitung biomassa ikan (kg) dan mencatatnya di riwayat."""
    if riwayat is None:
        riwayat = []
    biomassa_kg = jumlah * bobot_g / 1000
    if jumlah <= 0:
        print("jumlah tidak masuk akal")
    riwayat.append(biomassa_kg)
    print(f"Biomassa: {biomassa_kg} kg")
    return biomassa_kg


class KolamIkan:
    def __init__(self, nama):
        self.nama = nama

--- ruff check ---
All checks passed!
--- ruff format --check ---
1 file already formatted

Apa yang kita putuskan sendiri: riwayat=None (list dibuat baru di dalam fungsi), l menjadi biomassa_kg (nama yang bercerita), variabel tak terpakai dihapus, == True diganti pemeriksaan yang masuk akal (jumlah <= 0; maksud kode aslinya memang tidak jelas), % menjadi f-string, dan kolam_ikan menjadi KolamIkan. Semua pemeriksaan kini lulus (“All checks passed!”).

TipTIPS

Biasakan dua perintah ini sebelum menyimpan pekerjaan: ruff check --fix . lalu ruff format ..

pre-commit: penjaga pintu otomatis

Anda pasti suatu saat lupa menjalankan ruff. pre-commit adalah alat yang memasang “penjaga” di git: setiap kali Anda git commit (menyimpan perubahan; dibahas di Bab 20), serangkaian pemeriksaan berjalan otomatis. Bila ada yang gagal, commit dibatalkan. Pasang dengan python -m pip install pre-commit.

Pengaturannya di berkas .pre-commit-config.yaml. Pada dokumentasi resminya (pre-commit.com, dibaca 2026-10-10), kebanyakan contoh mengambil hook dari repo di internet (repo: https://... dengan rev:). Agar skrip kita tidak bergantung pada internet, di sini dipakai hook lokal (repo: local) yang menjalankan ruff yang sudah ada di komputer. Skrip berikut membuat repo git sementara di _tmp/, memasang penjaga, lalu mencoba dua commit.

07_pre_commit.py
# Bab 19 - pre-commit: pemeriksaan otomatis setiap kali git commit (di repo sementara)
import os
import shutil
import subprocess
import sys
from pathlib import Path

from bantu import FOLDER, hapus_folder

repo = FOLDER / "_tmp" / "repo_pre_commit"
hapus_folder(repo)
repo.mkdir(parents=True)
config_kosong = repo.parent / "gitconfig_kosong"
config_kosong.write_text("", encoding="utf-8")

# Git sementara: tidak membaca konfigurasi git pengguna; identitas lewat variabel lingkungan
folder_python = str(Path(sys.executable).parent)
env = dict(
    os.environ,
    GIT_CONFIG_GLOBAL=str(config_kosong), GIT_CONFIG_NOSYSTEM="1",
    GIT_AUTHOR_NAME="Uji", GIT_AUTHOR_EMAIL="uji@contoh.invalid",
    GIT_COMMITTER_NAME="Uji", GIT_COMMITTER_EMAIL="uji@contoh.invalid",
    PATH=folder_python + os.pathsep + os.environ["PATH"],     # supaya perintah 'ruff' ditemukan
    PYTHONUTF8="1", NO_COLOR="1",
)


def jalan(*perintah):
    hasil = subprocess.run(perintah, cwd=repo, env=env, text=True, encoding="utf-8",
                           stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    return hasil.stdout.strip(), hasil.returncode


(repo / ".pre-commit-config.yaml").write_text("""\
repos:
  - repo: local
    hooks:
      - id: ruff-periksa
        name: ruff periksa
        entry: ruff check
        language: unsupported
        types: [python]
      - id: ruff-format
        name: ruff format
        entry: ruff format --check
        language: unsupported
        types: [python]
""", encoding="utf-8")
shutil.copy(FOLDER / "contoh" / "berantakan.py", repo / "berantakan.py")
shutil.copy(FOLDER / "contoh" / "pyproject.toml", repo / "pyproject.toml")

jalan("git", "init", "--quiet", "-b", "main")
print(jalan(sys.executable, "-m", "pre_commit", "install")[0].replace(str(repo), "<repo>"))

jalan("git", "add", ".")
teks, kode = jalan("git", "commit", "-m", "tambah berantakan.py")
print("--- commit pertama (kode berantakan) ---")
print("\n".join(b for b in teks.splitlines() if b.startswith(("ruff", "- hook id", "- exit code"))))
print("commit berhasil?", kode == 0)

shutil.copy(FOLDER / "contoh" / "rapi.py", repo / "berantakan.py")
jalan("git", "add", ".")
teks, kode = jalan("git", "commit", "-m", "perbaiki berantakan.py")
print("--- commit kedua (kode sudah rapi) ---")
print("\n".join(b for b in teks.splitlines() if b.startswith("ruff")))
print("commit berhasil?", kode == 0)
print("--- riwayat ---")
print(jalan("git", "log", "--format=%s")[0])
pre-commit installed at .git\hooks\pre-commit
--- commit pertama (kode berantakan) ---
ruff periksa.............................................................Failed
- hook id: ruff-periksa
- exit code: 1
ruff format..............................................................Failed
- hook id: ruff-format
- exit code: 1
commit berhasil? False
--- commit kedua (kode sudah rapi) ---
ruff periksa.............................................................Passed
ruff format..............................................................Passed
commit berhasil? True
--- riwayat ---
perbaiki berantakan.py

Jalan ceritanya:

  • pre-commit install menaruh skrip penjaga di .git/hooks/pre-commit.
  • Commit pertama (kode berantakan): kedua hook menjawab Failed, dan commit tidak terjadi (False).
  • Berkas diganti dengan versi rapi, lalu commit kedua: keduanya Passed, commit berhasil, dan riwayat git berisi satu entri.

Dokumentasi pre-commit mencatat bahwa nilai language: system kini hanya alias bagi unsupported (sejak versi 4.4.0) dan akan dihapus; konfigurasi baru sebaiknya memakai unsupported, seperti di skrip ini. Untuk pre-commit versi lebih lama, ganti ke system. Repo sementara ini memakai git yang dipisahkan dari konfigurasi git Anda (lihat Bab 20).

Logging: catatan harian program

print() cocok untuk percobaan, tetapi di program sungguhan Anda butuh yang lebih teratur: logging, modul bawaan untuk mencatat peristiwa dengan tingkat kepentingan. Menurut dokumentasi Python (HOWTO Logging, dibaca 2026-10-10):

Tingkat Nilai Pemakaian
DEBUG 10 detail teknis untuk mencari bug
INFO 20 penegasan bahwa semuanya berjalan wajar
WARNING 30 sesuatu yang tak terduga, program masih jalan
ERROR 40 ada fungsi yang gagal dikerjakan
CRITICAL 50 kesalahan serius

Pencatat hanya meneruskan pesan yang tingkatnya sama atau lebih tinggi dari tingkat yang berlaku. Tingkat bawaan pencatat induk (root) adalah WARNING, sehingga tanpa pengaturan INFO dan DEBUG tidak tampil.

05_logging_dasar.py
# Bab 19 - logging: catatan harian program dengan tingkat kepentingan
import logging
import sys

logging.basicConfig(
    level=logging.INFO,
    format="%(levelname)-8s | %(name)s | %(message)s",
    stream=sys.stdout,
)
catat = logging.getLogger("kolam")

catat.debug("detail teknis (tidak tampil pada level INFO)")
catat.info("kolam A: 800 ikan ditebar")
catat.warning("pH 5.9 di bawah batas 6.5")
catat.error("sensor suhu tidak menjawab")

try:
    rata_rata = 540.5 / 0
except ZeroDivisionError:
    catat.exception("hitungan rata-rata gagal")      # sekaligus mencetak traceback

print("level yang berlaku:", logging.getLevelName(catat.getEffectiveLevel()))
INFO     | kolam | kolam A: 800 ikan ditebar
WARNING  | kolam | pH 5.9 di bawah batas 6.5
ERROR    | kolam | sensor suhu tidak menjawab
ERROR    | kolam | hitungan rata-rata gagal
Traceback (most recent call last):
  File "05_logging_dasar.py", line 18, in <module>
    rata_rata = 540.5 / 0
                ~~~~~~^~~
ZeroDivisionError: division by zero
level yang berlaku: INFO
  • basicConfig(level=..., format=..., stream=...) mengatur tingkat dan bentuk catatan. Bentuk %(levelname)-8s artinya nama tingkat, rata kiri, selebar 8 huruf.
  • Pesan DEBUG tidak tampil karena tingkat diatur INFO. Tiga lainnya tampil.
  • catat.exception(...) dipanggil di dalam except: ia mencatat pesan pada tingkat ERROR sekaligus traceback error-nya. Inilah pengganti print yang paling berguna.
  • getEffectiveLevel() menunjukkan tingkat yang berlaku: INFO.

Logging di paket inventaris

Dokumentasi Python menyarankan tiap modul memakai logger = logging.getLogger(__name__). Nama pencatat lalu mengikuti hierarki paket (inventaris.unit adalah anak inventaris), dan pesan naik ke pencatat induknya. Dokumentasi juga menyarankan pustaka tidak memasang pengatur keluaran sendiri: itu urusan aplikasi pemakai. Itulah yang kita ikuti: paket inventaris hanya mencatat (logger.debug di Unit.tambah, logger.info di simpan_ringkasan), dan skrip memutuskan ke mana catatan pergi.

06_logging_proyek.py
# Bab 19 - logging di paket inventaris: layar menampilkan INFO, berkas menyimpan DEBUG
import logging
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent / "proyek"))

from inventaris.contoh import buat_contoh  # noqa: E402
from inventaris.laporan import simpan_ringkasan  # noqa: E402

kerja = Path(__file__).parent / "_tmp"
kerja.mkdir(exist_ok=True)
berkas_log = kerja / "inventaris.log"
berkas_log.unlink(missing_ok=True)

format_log = "%(levelname)s %(name)s: %(message)s"
layar = logging.StreamHandler(sys.stdout)
layar.setLevel(logging.INFO)
berkas = logging.FileHandler(berkas_log, encoding="utf-8")
berkas.setLevel(logging.DEBUG)
for pencatat in (layar, berkas):
    pencatat.setFormatter(logging.Formatter(format_log))

akar = logging.getLogger("inventaris")      # induk dari inventaris.unit dan inventaris.laporan
akar.setLevel(logging.DEBUG)
akar.addHandler(layar)
akar.addHandler(berkas)

unit = buat_contoh()
simpan_ringkasan(unit, kerja / "ringkasan.txt")
berkas.close()

isi = berkas_log.read_text(encoding="utf-8").splitlines()
print(f"Isi berkas log: {len(isi)} baris")
print("Baris pertama :", isi[0])
print("Baris terakhir:", isi[-1])
INFO inventaris.laporan: menulis 5 ringkasan ke ringkasan.txt
Isi berkas log: 11 baris
Baris pertama : DEBUG inventaris.unit: Plot hutan A menerima Pohon
Baris terakhir: INFO inventaris.laporan: menulis 5 ringkasan ke ringkasan.txt

Dua penampung (handler) dipasang pada pencatat inventaris: layar menerima tingkat INFO ke atas, dan berkas menerima semuanya sampai DEBUG. Hasilnya: layar hanya menampilkan satu baris (menulis 5 ringkasan ...), sedangkan berkas log berisi 11 baris: 10 catatan DEBUG (tiap kali tambah dipanggil: 3 pohon + 2 + 1 + 1 + 3 = 10) ditambah 1 catatan INFO.

Debugging: mencari bug dengan teliti

Debugging adalah menjalankan program sambil berhenti di titik tertentu untuk melihat isi variabel. Cara paling dasar sudah ada di Python: fungsi bawaan breakpoint() menghentikan program dan membuka debugger pdb di terminal.

10_debug_contoh.py
# Bab 19 - Tempat memasang titik henti (breakpoint) saat mencari bug
def biomassa_kg(jumlah, bobot_g):
    total_g = jumlah * bobot_g
    breakpoint()                  # program berhenti di sini; ketik "p total_g" lalu "c" untuk lanjut
    return total_g / 1000


print(biomassa_kg(800, 540.5))

Kode ini tidak dijalankan saat buku dibuat karena breakpoint() menghentikan program dan menunggu perintah dari pengguna, sehingga tidak cocok untuk build otomatis. Penulis mencobanya sekali dengan masukan p total_g lalu c: program berhenti di baris breakpoint(), p total_g mencetak 432400.0 (gram), dan c (continue) melanjutkan hingga mencetak 432.4.

Di VS Code, cara yang lebih nyaman (deskripsi dari dokumentasi resmi VS Code, dibaca 2026-10-10; tidak dijalankan oleh penulis):

  • Pasang ekstensi Python (ekstensi Python Debugger ikut terpasang bersamanya).
  • Klik di margin kiri editor untuk memasang breakpoint (titik henti). Breakpoint bisa diberi syarat atau hitungan, misalnya hanya berhenti setelah lima kali.
  • Tekan F5 (Start Debugging) atau pilih Debug Python File. Program berhenti di breakpoint, dan Anda bisa memeriksa variabel.
  • Pengaturan lebih rinci disimpan di .vscode/launch.json (nama, program yang dijalankan, args, dan lain-lain).

Di notebook Jupyter dalam VS Code (juga dari dokumentasi resmi, tidak dijalankan): tersedia Run by Line untuk menjalankan sel satu pernyataan demi satu pernyataan, dan Debug Cell untuk debugger penuh dengan breakpoint. Keduanya membutuhkan ipykernel versi 6 ke atas.

PeringatanAWAS

Debugger menjawab pertanyaan “mengapa nilainya begini di titik ini?”. Jangan memakainya sebagai pengganti tes. Tes (Bab 18) mencegah bug yang sama muncul lagi; debugger hanya membantu menemukannya sekali.

Dokumentasi proyek: MkDocs dan Sphinx

Kode tanpa petunjuk pemakaian cepat dilupakan. Dua alat populer mengubah berkas teks menjadi situs dokumentasi:

  • MkDocs (mkdocs.org): menulis halaman dalam Markdown, lalu mkdocs build menghasilkan situs HTML statis di folder site/. mkdocs serve membuka pratinjau lokal. Satu-satunya pengaturan wajib di mkdocs.yml adalah site_name; nav mengatur urutan menu (dokumentasi resmi, dibaca 2026-10-10).
  • Sphinx (sphinx-doc.org): alat yang lebih tua dan lebih kaya, memakai reStructuredText. sphinx-quickstart membuat kerangka (conf.py, index.rst), make html membangun situs, dan ekstensi autodoc menarik dokumentasi langsung dari docstring (dokumentasi resmi, dibaca 2026-10-10). Sphinx tidak dipasang atau dijalankan di bab ini.

Skrip berikut membangun situs MkDocs mini sungguhan:

08_mkdocs.py
# Bab 19 - MkDocs: halaman dokumentasi dari berkas Markdown (membangun situs sungguhan di _tmp)
import shutil
import sys
from pathlib import Path

from bantu import FOLDER, jalankan

situs = FOLDER / "_tmp" / "situs_dokumentasi"
shutil.rmtree(situs, ignore_errors=True)
(situs / "docs").mkdir(parents=True)

(situs / "mkdocs.yml").write_text("""\
site_name: Dokumentasi Inventaris
nav:
  - Beranda: index.md
  - Cara memakai: cara-memakai.md
""", encoding="utf-8")
(situs / "docs" / "index.md").write_text(
    "# Inventaris\n\nPaket contoh untuk mencatat hutan, kebun, sawah, kolam, dan kandang.\n",
    encoding="utf-8")
(situs / "docs" / "cara-memakai.md").write_text(
    "# Cara memakai\n\n```python\nfrom inventaris.contoh import buat_contoh\n```\n",
    encoding="utf-8")

teks, kode = jalankan([sys.executable, "-m", "mkdocs", "build", "--quiet"], situs)
print("kode keluar:", kode, "| pesan:", teks or "(tidak ada)")

hasil = sorted(p.relative_to(situs / "site").as_posix() for p in (situs / "site").rglob("*.html"))
print("Halaman HTML yang dihasilkan:", hasil)
html = (situs / "site" / "index.html").read_text(encoding="utf-8")
print("Judul situs ada di halaman?", "Dokumentasi Inventaris" in html)
kode keluar: 0 | pesan: (tidak ada)
Halaman HTML yang dihasilkan: ['404.html', 'cara-memakai/index.html', 'index.html']
Judul situs ada di halaman? True

Kode keluar 0 dan tanpa pesan berarti pembangunan lancar. Tiga halaman HTML dihasilkan: index.html dan cara-memakai/index.html dari dua berkas Markdown kita, ditambah 404.html yang dibuat MkDocs sendiri. Judul situs dari mkdocs.yml memang tertulis di halaman.

CatatanINGAT

Docstring yang Anda tulis sejak Bab 10 adalah dokumentasi paling murah: ia ikut terbaca oleh help(), editor, dan alat seperti Sphinx autodoc. Menulis docstring satu kalimat di tiap fungsi dan kelas sudah banyak membantu.

Proyek Bersambung I1: merapikan inventaris

Sekarang semua alat dipakai pada paket inventaris, yang sudah memiliki logging dan 14 tes (satu tes baru memeriksa catatan log dengan fixture caplog). Skrip menyalin proyek ke _tmp/, mengecek, memperbaiki, dan mengujinya ulang.

09_proyek_rapi.py
# Bab 19 - Proyek bersambung I1: merapikan paket inventaris dengan ruff, lalu menguji ulang
import re
import shutil
import sys

from bantu import FOLDER, jalankan, ruff

kerja = FOLDER / "_tmp" / "proyek_rapi"
shutil.rmtree(kerja, ignore_errors=True)
shutil.copytree(FOLDER / "proyek", kerja, ignore=shutil.ignore_patterns("__pycache__"))


def periksa(judul):
    teks, _ = ruff("check", "--output-format", "concise", ".", folder=kerja)
    print(f"--- {judul} ---")
    print(teks)


periksa("ruff check, sebelum")

ruff("check", "--fix", "--quiet", ".", folder=kerja)     # perbaikan aman otomatis (urutan impor)
print("--- ruff format ---")
print(ruff("format", ".", folder=kerja)[0])
periksa("ruff check, setelah perbaikan otomatis")

# Dua docstring terlalu panjang: ruff tidak mau menebak kalimat Anda, jadi dipersingkat dengan tangan
perbaikan = {
    "inventaris/__init__.py": (
        "Paket inventaris: sistem informasi hutan, kebun, sawah, kolam, dan kandang (data fiktif).",
        "Paket inventaris: data fiktif hutan, kebun, sawah, kolam, dan kandang.",
    ),
    "inventaris/barang.py": (
        "Besar bila diameter 40 cm ke atas, sedang bila 30 cm ke atas, selain itu kecil.",
        "Besar mulai 40 cm, sedang mulai 30 cm, selain itu kecil.",
    ),
}
for jalur, (lama, baru) in perbaikan.items():
    berkas = kerja / jalur
    berkas.write_text(berkas.read_text(encoding="utf-8").replace(lama, baru), encoding="utf-8")
periksa("ruff check, setelah perbaikan tangan")
print(ruff("format", "--check", ".", folder=kerja)[0])

teks, kode = jalankan([sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider"], kerja)
print("--- pytest ---")
print(re.sub(r" in \d+\.\d+s", "", teks.splitlines()[-1]))
--- ruff check, sebelum ---
inventaris\__init__.py:1:89: E501 Line too long (95 > 88)
inventaris\barang.py:14:89: E501 Line too long (93 > 88)
inventaris\contoh.py:3:1: I001 [*] Import block is un-sorted or un-formatted
inventaris\contoh.py:3:89: E501 Line too long (92 > 88)
inventaris\unit.py:22:89: E501 Line too long (92 > 88)
Found 5 errors.
[*] 1 fixable with `--fix`.
--- ruff format ---
3 files reformatted, 6 files left unchanged
--- ruff check, setelah perbaikan otomatis ---
inventaris\__init__.py:1:89: E501 Line too long (95 > 88)
inventaris\barang.py:14:89: E501 Line too long (93 > 88)
Found 2 errors.
--- ruff check, setelah perbaikan tangan ---
All checks passed!
9 files already formatted
--- pytest ---
14 passed

Telusuri:

  • Sebelum: 5 temuan, hampir semuanya E501 (baris lebih dari 88 karakter) dan satu I001 (urutan impor).
  • --fix dan format menyelesaikan tiga temuan (impor dirapikan, baris panjang dipecah per baris). Sisa 2 adalah docstring yang terlalu panjang: ruff tidak mau menebak kalimat Anda, jadi kita persingkat dengan tangan.
  • Sesudahnya semua pemeriksaan lulus, 9 berkas rapi, dan 14 tes tetap lulus: merapikan tidak merusak perilaku. Itulah gunanya tes di Bab 18.

Latihan

  1. Jalankan ruff check pada skrip lama Anda (misalnya dari Bab 10). Berapa temuan? Pilih satu kode dan cari artinya dengan python -m ruff rule F841.
  2. Ubah 05_logging_dasar.py agar tingkatnya DEBUG. Pesan apa yang bertambah?
  3. Tambahkan logger.warning(...) pada Kolam bila ph_air di luar 6,5 sampai 8,5, lalu amati lewat 06_logging_proyek.py. (Batas ini hanya angka contoh.)
  4. Tantangan: ubah 07_pre_commit.py supaya berkas pertama hanya berisi import os dan satu fungsi rapi. Hook mana yang gagal, dan hook mana yang lolos? Jelaskan alasannya.

Ringkasan Bab

  • PEP 8 adalah panduan gaya resmi Python: konsisten dan mudah dibaca.
  • ruff check memeriksa (kode E, F, I, B, …), --fix hanya memperbaiki yang aman, ruff format menata tampilan tanpa mengubah perilaku.
  • pre-commit menjalankan pemeriksaan otomatis tiap git commit dan membatalkan commit bila gagal.
  • logging mencatat dengan tingkat DEBUG, INFO, WARNING, ERROR, CRITICAL; modul memakai getLogger(__name__), aplikasi yang memilih tujuan catatan.
  • Debugging: breakpoint() (pdb) atau fitur VS Code dan Jupyter; mencari bug, bukan pengganti tes.
  • MkDocs (Markdown) dan Sphinx (reStructuredText) membuat situs dokumentasi dari berkas teks dan docstring.

Selanjutnya, di Bab 20: git, GitHub, dan alur kerja proyek: menyimpan riwayat, bercabang, bekerja bersama, dan menutup Seri I1.