Bab 45 — Pengemasan: Dari Folder Kode Menjadi Paket
Seri A1 — Rekayasa Perangkat Lunak Profesional
Semua skrip bab ini ada di folder kode/bab45/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Di Bab 44, rekap nilai kayu kita sudah cepat. Tetapi kodenya masih berupa berkas lepas. Bila rekan kerja ingin memakainya, Anda harus mengirim berkas satu per satu, dan mereka bingung versi mana yang terbaru. Bayangkan Anda menjual bibit: bibit lepas di karung itu merepotkan, sedangkan bibit dalam kemasan berlabel (nama, versi, cara tanam) mudah dikirim dan dipercaya. Paket Python adalah kemasan itu: kode ditambah label (metadata) yang dipahami alat seperti pip.

Struktur folder paket
01_struktur.py
# Bab 45 - Struktur folder paket (tata letak "src")
from pathlib import Path
akar = Path(__file__).parent / "proyek_paket"
print(akar.name + "/")
for berkas in sorted(akar.rglob("*"), key=lambda p: p.as_posix().lower()):
if "__pycache__" in berkas.parts or "_tmp" in berkas.parts or berkas.suffix == ".egg-info":
continue
kedalaman = len(berkas.relative_to(akar).parts)
tanda = "/" if berkas.is_dir() else ""
print(" " * kedalaman + berkas.name + tanda)proyek_paket/
pyproject.toml
README.md
src/
pantau_lapangan/
__init__.py
cli.py
hitung.py
tests/
test_hitung.py
Ini disebut tata letak src: kode paket ada di dalam src/pantau_lapangan/, terpisah dari tests/. Keuntungannya: saat menguji, Anda menguji paket yang benar-benar diimpor seperti pengguna, bukan folder kerja yang kebetulan ada di sekitar. Tiap bagian:
pyproject.toml: kartu identitas dan cara membangun paket.README.md: penjelasan untuk pengguna; ikut menjadi halaman utama paket.__init__.py: tanda bahwa folder itu sebuah package, dan tempat memilih apa yang boleh diimpor orang.cli.py: antarmuka perintah terminal.
pyproject.toml: kartu identitas paket
Berkas pyproject.toml memakai format TOML (teks sederhana berbentuk kunci = nilai). Isinya:
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
[project]
name = "pantau-lapangan-contoh"
version = "0.1.0"
description = "Paket contoh dari buku: rekap nilai kayu per plot dan jenis"
readme = "README.md"
requires-python = ">=3.12"
license = "MIT"
dependencies = []
[project.optional-dependencies]
cepat = ["numpy>=2"]
uji = ["pytest>=8"]
[project.scripts]
pantau-lapangan = "pantau_lapangan.cli:main"(Potongan di atas disingkat; berkas aslinya ada di zip dan juga menambah classifiers dan pencarian paket di src.) Python punya pembaca TOML bawaan, tomllib:
02_pyproject.py
# Bab 45 - Membaca pyproject.toml dengan tomllib (bawaan Python 3.11+)
import tomllib
from pathlib import Path
isi = tomllib.loads((Path(__file__).parent / "proyek_paket" / "pyproject.toml").read_text(encoding="utf-8"))
proyek = isi["project"]
print("nama paket :", proyek["name"])
print("versi :", proyek["version"])
print("butuh Python :", proyek["requires-python"])
print("dependensi :", proyek["dependencies"])
print("tambahan opsional:", sorted(proyek["optional-dependencies"]))
print("perintah terminal:", proyek["scripts"])
print("alat build :", isi["build-system"]["requires"], "->", isi["build-system"]["build-backend"])nama paket : pantau-lapangan-contoh
versi : 0.1.0
butuh Python : >=3.12
dependensi : []
tambahan opsional: ['cepat', 'uji']
perintah terminal: {'pantau-lapangan': 'pantau_lapangan.cli:main'}
alat build : ['setuptools>=77'] -> setuptools.build_meta
Arti bagian pentingnya:
| Bagian | Fungsi |
|---|---|
[build-system] |
alat yang dipakai untuk membangun paket (di sini setuptools) |
name, version |
nama dan nomor versi paket |
requires-python |
versi Python minimal |
dependencies |
paket lain yang wajib ada (di sini kosong) |
optional-dependencies |
tambahan opsional, dipasang dengan pip install "paket[cepat]" |
[project.scripts] |
membuat perintah terminal pantau-lapangan yang memanggil fungsi main |
Nama paket di atas sengaja memakai akhiran -contoh supaya tidak bentrok dengan paket sungguhan. Sebelum memberi nama paket Anda sendiri, periksa dulu apakah namanya sudah dipakai di PyPI (gudang paket Python). Nomor versi mengikuti pola MAJOR.MINOR.PATCH: naikkan PATCH untuk perbaikan kecil, MINOR untuk fitur baru yang kompatibel, MAJOR bila ada perubahan yang bisa merusak kode pengguna.
Memakai paket langsung dari src
Sebelum dipasang, paket bisa dicoba dengan menaruh src ke jalur impor Python:
03_impor_dari_src.py
# Bab 45 - Memakai paket langsung dari folder src (belum dipasang)
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "proyek_paket" / "src"))
import pantau_lapangan
pohon = [
{"jenis": "Jati", "plot": "A", "diameter_cm": 32.5, "tinggi_m": 18.0},
{"jenis": "Meranti", "plot": "A", "diameter_cm": 45.2, "tinggi_m": 22.0},
{"jenis": "Jati", "plot": "B", "diameter_cm": 28.0, "tinggi_m": 15.0},
]
print("versi (belum dipasang):", pantau_lapangan.__version__)
for kunci, nilai in pantau_lapangan.nilai_per_kelompok(pohon).items():
print(kunci, f"Rp {nilai:,.0f}")versi (belum dipasang): belum-dipasang
('A', 'Jati') Rp 2,986,474
('A', 'Meranti') Rp 4,412,646
('B', 'Jati') Rp 1,847,255
Perhatikan versinya: belum-dipasang. Di __init__.py, versi dibaca dari metadata paket yang terpasang (importlib.metadata), jadi satu-satunya sumber nomor versi adalah pyproject.toml. Bila belum dipasang, kita menampilkan nilai pengganti, bukan error.
Membangun: sdist dan wheel
Perintah python -m build menghasilkan dua berkas di folder dist/:
- sdist (source distribution,
.tar.gz): arsip kode sumber. - wheel (
.whl): paket jadi yang siap dipasang cepat olehpip.
Kita membangun di folder sementara _tmp/ supaya folder kerja tetap bersih:
04_build.py
# Bab 45 - Membangun paket (python -m build) di folder sementara _tmp/
import shutil
import subprocess
import sys
from pathlib import Path
akar = Path(__file__).parent
sumber = akar / "proyek_paket"
kerja = akar / "_tmp" / "proyek"
shutil.rmtree(akar / "_tmp", ignore_errors=True)
shutil.copytree(sumber, kerja, ignore=shutil.ignore_patterns("__pycache__", "*.egg-info", "_tmp"))
# --no-isolation: memakai setuptools yang sudah terpasang, jadi tidak perlu internet.
# Tanpa opsi itu (cara biasa), "build" membuat lingkungan terpisah dan mengunduh alat build.
hasil = subprocess.run(
[sys.executable, "-m", "build", "--no-isolation", str(kerja)],
capture_output=True, text=True, encoding="utf-8",
)
print("kode keluar build:", hasil.returncode)
for baris in hasil.stdout.splitlines():
if baris.startswith("Successfully built"):
print(baris)
print("isi folder dist:")
for berkas in sorted((kerja / "dist").iterdir()):
print(" ", berkas.name)kode keluar build: 0
Successfully built pantau_lapangan_contoh-0.1.0.tar.gz and pantau_lapangan_contoh-0.1.0-py3-none-any.whl
isi folder dist:
pantau_lapangan_contoh-0.1.0-py3-none-any.whl
pantau_lapangan_contoh-0.1.0.tar.gz
Perintah normalnya hanya python -m build. Perintah itu membuat lingkungan terisolasi lalu mengunduh alat build dari internet. Skrip kita memakai --no-isolation agar berjalan tanpa internet dengan setuptools yang sudah terpasang. Di komputer Anda cukup python -m build. (Anda perlu memasang alat build dahulu dengan pip install build.)
twine check dan isi wheel
twine check memeriksa apakah metadata paket valid dan README-nya dapat ditampilkan benar di PyPI. Ia hanya memeriksa; tidak mengunggah apa pun.
05_twine_check.py
# Bab 45 - twine check: memeriksa metadata paket (TIDAK mengunggah apa pun)
import os
import re
import subprocess
import sys
from pathlib import Path
dist = Path(__file__).parent / "_tmp" / "proyek" / "dist"
berkas = sorted(str(p) for p in dist.iterdir())
# COLUMNS lebar dan NO_COLOR agar keluaran twine tidak terpotong atau berwarna
env = dict(os.environ, COLUMNS="400", NO_COLOR="1")
hasil = subprocess.run(
[sys.executable, "-m", "twine", "check", *berkas],
capture_output=True, text=True, encoding="utf-8", env=env,
)
teks = re.sub(r"\s+", " ", hasil.stdout)
for nama, status in re.findall(r"Checking \S*?([\w.\-]+\.(?:whl|tar\.gz)): (\w+)", teks):
print(f"{nama}: {status}")
print("kode keluar twine check:", hasil.returncode)pantau_lapangan_contoh-0.1.0-py3-none-any.whl: PASSED
pantau_lapangan_contoh-0.1.0.tar.gz: PASSED
kode keluar twine check: 0
Wheel ternyata hanya berkas zip. Mari mengintip isinya:
06_isi_wheel.py
# Bab 45 - Mengintip isi berkas wheel (sebenarnya sebuah zip)
import zipfile
from pathlib import Path
dist = Path(__file__).parent / "_tmp" / "proyek" / "dist"
wheel = next(dist.glob("*.whl"))
print("nama wheel:", wheel.name)
with zipfile.ZipFile(wheel) as z:
for nama in sorted(z.namelist()):
print(" ", nama)
metadata = z.read(next(n for n in z.namelist() if n.endswith("METADATA"))).decode("utf-8")
print("--- sebagian METADATA ---")
for baris in metadata.splitlines()[:8]:
print(baris)nama wheel: pantau_lapangan_contoh-0.1.0-py3-none-any.whl
pantau_lapangan/__init__.py
pantau_lapangan/cli.py
pantau_lapangan/hitung.py
pantau_lapangan_contoh-0.1.0.dist-info/METADATA
pantau_lapangan_contoh-0.1.0.dist-info/RECORD
pantau_lapangan_contoh-0.1.0.dist-info/WHEEL
pantau_lapangan_contoh-0.1.0.dist-info/entry_points.txt
pantau_lapangan_contoh-0.1.0.dist-info/top_level.txt
--- sebagian METADATA ---
Metadata-Version: 2.4
Name: pantau-lapangan-contoh
Version: 0.1.0
Summary: Paket contoh dari buku: rekap nilai kayu per plot dan jenis
License-Expression: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.12
Yang masuk hanya kode paket (pantau_lapangan/) dan folder dist-info berisi metadata. Folder tests/ tidak ikut, karena kita memang hanya mencari paket di src. Baris METADATA memuat nama, versi, ringkasan, dan Requires-Python yang kita tulis di pyproject.toml.
Memasang dan memakai
Bukti terbaik sebuah paket berfungsi adalah memasangnya di tempat bersih, seolah Anda pengguna lain. Skrip berikut membuat lingkungan virtual baru, memasang wheel, lalu menjalankan perintah terminalnya:
07_pasang_dan_pakai.py
# Bab 45 - Proyek bersambung A1: memasang wheel ke lingkungan virtual BARU lalu memakainya
import subprocess
import sys
from pathlib import Path
akar = Path(__file__).parent / "_tmp"
wheel = next((akar / "proyek" / "dist").glob("*.whl"))
venv = akar / "venv_uji"
bin_dir = venv / ("Scripts" if sys.platform == "win32" else "bin")
exe = ".exe" if sys.platform == "win32" else ""
subprocess.run([sys.executable, "-m", "venv", str(venv)], check=True)
python_baru = str(bin_dir / f"python{exe}")
# --no-index --no-deps: tidak ke internet, dan paket kita memang tanpa dependensi
pasang = subprocess.run(
[python_baru, "-m", "pip", "install", "--no-index", "--no-deps", "--quiet", str(wheel)],
capture_output=True, text=True,
)
print("pasang berhasil:", pasang.returncode == 0)
versi = subprocess.run(
[python_baru, "-c", "import pantau_lapangan as p; print(p.__version__)"],
capture_output=True, text=True,
)
print("versi terpasang:", versi.stdout.strip())
cli = subprocess.run([str(bin_dir / f"pantau-lapangan{exe}"), "--jumlah", "1000"],
capture_output=True, text=True, encoding="utf-8")
print("--- keluaran perintah pantau-lapangan --jumlah 1000 ---")
print(cli.stdout.rstrip())pasang berhasil: True
versi terpasang: 0.1.0
--- keluaran perintah pantau-lapangan --jumlah 1000 ---
12 kelompok dari 1000 pohon
plot A Jati Rp 310,014,243
plot A Mahoni Rp 191,426,619
plot A Meranti Rp 167,648,188
Perintah pantau-lapangan tersedia karena bagian [project.scripts] tadi. Versi terpasang terbaca 0.1.0 dari metadata, bukan lagi belum-dipasang. Opsi --no-index --no-deps hanya kita pakai agar skrip tak butuh internet.
Menguji paket
08_uji_paket.py
# Bab 45 - Menjalankan tes paket dengan pytest (terhadap kode di folder src)
import os
import re
import subprocess
import sys
from pathlib import Path
proyek = Path(__file__).parent / "proyek_paket"
env = dict(os.environ, PYTHONPATH=str(proyek / "src"))
hasil = subprocess.run(
[sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider", str(proyek / "tests")],
capture_output=True, text=True, encoding="utf-8", env=env, cwd=str(proyek),
)
ringkas = re.search(r"(\d+) passed", hasil.stdout)
print("tes lulus:", ringkas.group(1) if ringkas else "tidak ada")
print("kode keluar pytest:", hasil.returncode)tes lulus: 2
kode keluar pytest: 0
Dua tes lulus. Kita menjalankan pytest terhadap kode di src lewat PYTHONPATH. Di proyek sungguhan, Anda biasanya memasang paket dalam mode terkembang (pip install -e .) lalu menjalankan pytest.
Alur publikasi: trusted publishing (tidak dijalankan)
Untuk membagikan paket ke semua orang, Anda mengunggahnya ke PyPI. Dokumentasi resmi (packaging.python.org, dibaca 2026-10-10) menganjurkan Trusted Publishing: alih-alih menyimpan token rahasia, PyPI mempercayai alur kerja GitHub Actions tertentu milik Anda. Ringkasnya:
- Di PyPI, daftarkan “publisher tepercaya”: repositori GitHub dan nama lingkungan (environment)
pypi. - Di GitHub, buat alur kerja yang membangun paket, lalu menerbitkannya hanya saat ada tag rilis.
- Alur kerja diberi izin
id-token: writesupaya GitHub bisa membuktikan identitasnya ke PyPI.
Potongan alur kerja dari dokumentasi resmi (disederhanakan; hanya bagian menerbitkan):
publish-to-pypi:
if: startsWith(github.ref, 'refs/tags/') # hanya saat tag rilis
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/<nama-paket>
permissions:
id-token: write # wajib untuk trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@v6
with:
name: python-package-distributions
path: dist/
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1Kode ini tidak dijalankan saat buku dibuat karena mengunggah ke PyPI mengubah dunia luar dan tidak boleh dilakukan untuk contoh buku, jadi keluarannya tidak ditampilkan. Kami tidak mengunggah paket contoh ini ke PyPI maupun TestPyPI.
Proyek Bersambung A1: rekap nilai kayu menjadi paket
Skrip 04 sampai 08 di atas sekaligus adalah langkah proyek: fungsi nilai_per_kelompok dari Bab 44 kini ada di paket pantau-lapangan-contoh versi 0.1.0, lolos twine check, terpasang di lingkungan bersih, bisa dipanggil lewat perintah pantau-lapangan, dan teruji (2 tes lulus). Versi paket ini sengaja tidak memakai numpy (cukup satu kali lewat data dengan dict), dan numpy ditaruh sebagai tambahan opsional cepat. Itu keputusan desain: paket yang punya sedikit dependensi lebih mudah dipasang. Di Bab 46, kita menata ulang arsitekturnya.
Latihan
- Ubah
versiondipyproject.toml(salinan Anda) menjadi0.1.1, bangun ulang, dan lihat nama berkas didist/. - Tambahkan perintah terminal kedua di
[project.scripts]yang memanggil fungsi lain. Pasang ulang dan cobalah. - Hapus
README.mddari salinan proyek lalu jalankanpython -m build. Apa yang terjadi? - Tantangan: tambahkan
[project.urls](misalnyaHomepage) dan opsi--jumlahbernilai tak valid (--jumlah abc) pada CLI; amati pesan galatargparseyang sudah ramah.
Ringkasan Bab
- Paket = kode + metadata. Tata letak
srcmemisahkan kode paket dari tes. pyproject.tomladalah satu berkas konfigurasi: build, nama, versi, dependensi, perintah terminal.python -m buildmenghasilkan sdist dan wheel;twine checkmemeriksa metadata tanpa mengunggah.- Buktikan paket dengan memasangnya di lingkungan bersih.
- Publikasi resmi memakai Trusted Publishing dari GitHub Actions; di buku ini hanya dijelaskan, tidak dijalankan.
Selanjutnya, di Bab 46: pola desain dan arsitektur, menata kode agar tetap rapi saat aplikasi membesar.