Sweep ruff tiga lapis: alias UTC, isort, artefak build
Satu sweep ruff ternyata menyentuh tiga lapis: alias datetime.UTC, blok TYPE_CHECKING plus batas isort alembic, dan artefak build yang merusak image.
Ringkasan
Jadi pas sweep ruff di pipeline CI, build Docker tiba-tiba gagal gara-gara error flat-layout. Ternyata urusannya tiga lapis: ganti datetime.now(timezone.utc) jadi datetime.now(UTC), atur klasifikasi import isort biar alembic dikenali, dan bersihkan folder build yang numpuk di image. Setelah semua beres plus konfigurasi pyproject.toml, pipeline balik hijau deh.
Momen merahnya
Layar terminal menampilkan baris error merah setelah proses instalasi masuk ke dalam image Docker. Pesannya spesifik: multiple top-level packages discovered in a flat-layout: app, build. Saya sedang menyusun pipeline continuous integration untuk proyek DemandScope, dan build yang sebelumnya stabil tiba-tiba gagal total.
Awalnya saya mengira ini hanya masalah sepele terkait gaya penulisan kode. Pesan dari linter sering kali terlihat seperti permintaan kosmetik belaka. Saya berasumsi cukup menambahkan konfigurasi isort untuk merapikan urutan import, dan pipeline akan kembali hijau.
Asumsi itu meleset. Sweep ruff pada DemandScope ternyata menyentuh tiga lapisan berbeda yang saling memengaruhi: runtime, struktur import model, dan artefak build di image.
Lapisan runtime dan struktur import
Mengganti datetime.now(timezone.utc) menjadi datetime.now(UTC) adalah pembersihan import yang diperkenalkan sejak Python 3.11[6]. Alias UTC lebih pendek dan resmi; aturan UP017 milik ruff menandai perubahan ini sebagai aman untuk diterapkan otomatis, kecuali ekspresinya mengandung komentar[6]. Pola lama itu terasa seperti peninggalan versi sebelumnya yang tidak lagi diperlukan.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from app.domain.models.user import User
class Tenant(Base):
# saat runtime baris import di atas tidak dieksekusi
user: Mapped["User"] = relationship()
Lapisan kedua berkaitan dengan struktur import model. Blok if TYPE_CHECKING: mengeksekusi isinya hanya saat type checking berjalan dan bernilai False saat runtime[7]. Ini cara resmi menahan import demi forward reference pada model SQLAlchemy yang saling menunjuk lewat relasi dua arah. Spesifikasi typing menyebut pola ini untuk kode yang harus terlihat oleh type checker tapi tidak boleh dieksekusi[7]. Tanpa blok ini, dua model yang saling memuat satu sama lain akan berakhir circular import dan aplikasi menolak jalan.
[tool.ruff]
line-length = 100
target-version = "py312"
extend-exclude = ["build"]
[tool.ruff.lint]
select = ["E", "F", "I", "W", "UP", "B"]
[tool.ruff.lint.isort]
known-first-party = ["app"]
# alembic diimpor file migrasi di dalam app, tapi bukan first-party
known-third-party = ["alembic"]
Bagian yang paling sering keliru dipahami: urutan import yang dituntut isort bergantung pada klasifikasi asal module, dan klasifikasi itu bisa salah bila ada module luar yang diimpor dari dalam package sendiri. isort juga butuh batasan yang jelas. Tanpa pengaturan known-third-party untuk alembic, module alembic yang diimpor dari file migrasi di dalam package app terdeteksi sebagai first-party. Akibatnya aturan urutan import I001 terus gagal pada file migrasi[8]. Konfigurasi ini fungsional, bukan kosmetik.
# sebelum: pola Python 3.10
from datetime import datetime, timedelta, timezone
now = datetime.now(timezone.utc)
# sesudah: alias resmi sejak Python 3.11
from datetime import UTC, datetime, timedelta
now = datetime.now(UTC)
Lapisan artefak build
Pesan error di awal cerita datang dari lapisan ketiga. Setuptools menolak menebak saat flat-layout berisi lebih dari satu kandidat package. Folder build di image adalah kandidat palsu yang dibuat oleh perintah install sendiri. Lapisan ketiga adalah penyebab error utama. Auto-discovery flat-layout milik setuptools menolak proses build bila menemukan lebih dari satu top-level package[9]. Folder build hasil pip install . di dalam image menjadi top-level package kedua pada build berikutnya, memicu error di atas. Solusinya membersihkan artefak di baris RUN yang sama, plus extend-exclude = ["build"] supaya ruff tidak pernah mem-parsing file hasil build yang tertinggal[9].
RUN pip install --no-cache-dir ".[dev]" && rm -rf build dist *.egg-info
# artefak build tidak boleh ikut menumpuk di layer image
Satu hal yang bikin sweep ini terasa aneh: pyproject.toml di repo adalah file baru. Sebelumnya tidak ada file konfigurasi sama sekali, jadi penambahan build-system setuptools dan seluruh blok tool.ruff datang sebagai satu paket dengan perbaikan lint-nya. Setelah tiga lapis ini beres, pipeline kembali hijau dan keputusan konfigurasinya berhenti menjadi sumber debat. Urutan import yang rapi itu efek samping; yang tersisa adalah batas first-party dan third-party yang eksplisit, alias datetime yang resmi, dan image tanpa sampah build.
Sources:
[6] Ruff rule UP017 (datetime-timezone-utc)
[7] Python typing spec: directives