1. Pengenalan Python Click
Click (Command Line Interface Creation Kit) adalah library Python untuk membuat command line interface (CLI) yang indah dan mudah dikembangkan. Click menggunakan pendekatan dekorator yang membuat kode CLI menjadi sangat bersih dan mudah dibaca.
Click vs argparse
| Aspek | Click | argparse (bawaan) |
|---|---|---|
| Pendekatan | Dekorator | Parser configuration |
| Kode | ๐ข Singkat dan bersih | ๐ก Verbose |
| Subcommands | ๐ข Sangat mudah | ๐ก Perlu setup tambahan |
| Prompts | ๐ข Built-in | ๐ด Perlu manual |
| Color/Formatting | ๐ข Built-in | ๐ด Perlu manual |
| Auto Help | ๐ข Otomatis dan indah | ๐ก Dasar |
| Dependencies | ๐ก Perlu instal | ๐ข Bawaan Python |
Arsitektur Click
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ CLICK ARCHITECTURE โ โ โ โ $ mycli greet --name Budi --count 3 โ โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ Command Pipeline โ โ โ โ โ โ โ โ 1. Parse Arguments โ โ โ โ โโโ "greet" โ command โ โ โ โ โโโ --name Budi โ option โ โ โ โ โโโ --count 3 โ option โ โ โ โ โ โ โ โ 2. Validate Types โ โ โ โ โโโ name โ str โ โ โ โ โ โโโ count โ int โ โ โ โ โ โ โ โ โ 3. Execute Command โ โ โ โ โโโ greet(name="Budi", count=3) โ โ โ โ โ โ โ โ 4. Output Result โ โ โ โ โโโ "Hello, Budi!" x3 โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ โ Komponen: โ โ โโโ @click.command() โ Membuat command โ โ โโโ @click.argument() โ Positional argument โ โ โโโ @click.option() โ Optional flag/parameter โ โ โโโ @click.group() โ Grup commands โ โ โโโ @click.pass_context โ Shared context โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
2. Instalasi dan Quick Start
# Instal Click pip install click # Verifikasi python -c "import click; print(click.__version__)" # Output: 8.x.x
CLI Pertama
import click
@click.command()
@click.option('--name', '-n', default='Dunia', help='Nama untuk disapa')
@click.option('--count', '-c', default=1, type=int, help='Jumlah pengulangan')
@click.option('--greeting', '-g', default='Halo', help='Salam pembuka')
def hello(name, count, greeting):
"""Program CLI pertama menggunakan Click.
Contoh: python hello_cli.py --name Budi --count 3
"""
for _ in range(count):
click.echo(f"{greeting}, {name}!")
if __name__ == '__main__':
hello()
# Jalankan dari terminal:
# $ python hello_cli.py
# Halo, Dunia!
# $ python hello_cli.py --name Budi --count 3
# Halo, Budi!
# Halo, Budi!
# Halo, Budi!
# $ python hello_cli.py -n Budi -c 2 -g "Selamat pagi"
# Selamat pagi, Budi!
# Selamat pagi, Budi!
# $ python hello_cli.py --help
# Usage: hello_cli.py [OPTIONS]
#
# Program CLI pertama menggunakan Click.
#
# Options:
# -n, --name TEXT Nama untuk disapa
# -c, --count INTEGER Jumlah pengulangan
# -g, --greeting TEXT Salam pembuka
# --help Show this message and exit.
3. Commands
Command adalah fungsi yang dijalankan saat user memanggil CLI. Setiap command bisa punya arguments dan options sendiri.
import click
# ========================================
# Basic Command
# ========================================
@click.command()
def init():
"""Inisialisasi project baru."""
click.echo("Menginisialisasi project...")
click.echo("โ
Project berhasil dibuat!")
# ========================================
# Command dengan Multiple Decorators
# ========================================
@click.command()
@click.argument('filename')
@click.option('--verbose', '-v', is_flag=True, help='Mode verbose')
@click.option('--output', '-o', type=click.Path(), help='Output file')
def process(filename, verbose, output):
"""Proses file yang diberikan."""
if verbose:
click.echo(f"Memproses file: {filename}")
# Proses file di sini...
result = f"Hasil dari {filename}"
if output:
with open(output, 'w') as f:
f.write(result)
click.echo(f"Output disimpan ke: {output}")
else:
click.echo(result)
# ========================================
# Command dengan Return Code
# ========================================
@click.command()
@click.argument('url')
def check(url):
"""Cek apakah URL bisa diakses."""
click.echo(f"Mengecek {url}...")
# Kembalikan exit code
# 0 = sukses, non-0 = error
raise SystemExit(0)
# ========================================
# Callback (before/after command)
# ========================================
@click.command()
@click.pass_context
def sync(ctx):
"""Sinkronisasi data."""
click.echo("๐ Memulai sinkronisasi...")
# Proses
click.echo("โ
Sinkronisasi selesai!")
# Menjalankan command
if __name__ == '__main__':
init()
4. Arguments
Arguments adalah parameter positional yang wajib diberikan (kecuali ada default). Berbeda dari options, arguments tidak punya prefix --.
import click
# ========================================
# Basic Argument
# ========================================
@click.command()
@click.argument('filename')
def cat(filename):
"""Tampilkan isi file."""
with open(filename) as f:
click.echo(f.read())
# Usage: python cat.py myfile.txt
# ========================================
# Argument dengan Type
# ========================================
@click.command()
@click.argument('count', type=int)
@click.argument('message')
def repeat(count, message):
"""Ulangi pesan sebanyak COUNT kali."""
for i in range(count):
click.echo(f"{i+1}. {message}")
# Usage: python repeat.py 3 "Hello World"
# ========================================
# Multiple Arguments
# ========================================
@click.command()
@click.argument('source')
@click.argument('destination')
@click.argument('filenames', nargs=-1) # -1 = variadic (bisa banyak)
def copy(source, destination, filenames):
"""Copy files dari SOURCE ke DESTINATION."""
click.echo(f"Source: {source}")
click.echo(f"Destination: {destination}")
for f in filenames:
click.echo(f" Copying: {f}")
# Usage: python copy.py /src /dest file1.txt file2.txt
# ========================================
# Argument Types
# ========================================
# File path (validasi otomatis)
@click.command()
@click.argument('input_file', type=click.Path(exists=True))
@click.argument('output_file', type=click.Path())
def convert(input_file, output_file):
"""Konversi file."""
click.echo(f"Konversi {input_file} โ {output_file}")
# Choice (pilihan terbatas)
@click.command()
@click.argument('format', type=click.Choice(['json', 'csv', 'xml']))
def export(format):
"""Export data dalam format tertentu."""
click.echo(f"Exporting dalam format: {format}")
# Int range
@click.command()
@click.argument('level', type=click.IntRange(0, 10))
def set_level(level):
"""Set level (0-10)."""
click.echo(f"Level diatur ke: {level}")
# Float range
@click.command()
@click.argument('temperature', type=click.FloatRange(-50, 100))
def set_temp(temperature):
"""Set suhu (-50 sampai 100)."""
click.echo(f"Suhu: {temperature}ยฐC")
# File type
@click.command()
@click.argument('input_file', type=click.File('r'))
@click.argument('output_file', type=click.File('w'))
def transform(input_file, output_file):
"""Transform file (auto-open/close)."""
content = input_file.read()
output_file.write(content.upper())
click.echo("Selesai!")
# ========================================
# Optional Argument (dengan default)
# ========================================
@click.command()
@click.argument('name', default='Dunia')
def greet(name):
"""Sapa seseorang."""
click.echo(f"Halo, {name}!")
# Usage:
# python greet.py โ Halo, Dunia!
# python greet.py Budi โ Halo, Budi!
# ========================================
# Required vs Optional
# ========================================
@click.command()
@click.argument('required_arg') # Wajib
@click.argument('optional_arg', required=False) # Opsional
def demo(required_arg, optional_arg):
"""Demo argument required vs optional."""
click.echo(f"Required: {required_arg}")
click.echo(f"Optional: {optional_arg or 'Tidak ada'}")
Ringkasan Tipe Argument
| Type | Kegunaan | Contoh |
|---|---|---|
str (default) | String biasa | click.argument('name') |
int | Integer | click.argument('n', type=int) |
float | Float | click.argument('x', type=float) |
click.Path() | File/directory path | click.argument('f', type=click.Path()) |
click.File() | Auto-open file | click.argument('f', type=click.File('r')) |
click.Choice() | Pilihan terbatas | click.argument('fmt', type=click.Choice(['a','b'])) |
click.IntRange() | Integer dengan range | click.argument('n', type=click.IntRange(0,100)) |
nargs=-1 | Variadic (banyak nilai) | click.argument('files', nargs=-1) |
5. Options
Options adalah parameter opsional yang dimulai dengan -- atau -. Options lebih fleksibel dari arguments dan mendukung banyak fitur lanjutan.
import click
# ========================================
# Option Dasar
# ========================================
@click.command()
@click.option('--name', '-n', help='Nama pengguna')
@click.option('--age', '-a', type=int, help='Umur pengguna')
@click.option('--verbose', '-v', is_flag=True, help='Mode verbose')
def user_info(name, age, verbose):
"""Tampilkan informasi user."""
if verbose:
click.echo("=== Debug Mode ===")
click.echo(f"Nama: {name or 'Anonim'}")
click.echo(f"Umur: {age or 'Tidak diketahui'}")
# Usage:
# python user.py --name Budi --age 25 -v
# python user.py -n Budi -a 25
# ========================================
# Option dengan Default Value
# ========================================
@click.command()
@click.option('--host', '-h', default='localhost', help='Server host')
@click.option('--port', '-p', default=8000, type=int, help='Server port')
@click.option('--debug/--no-debug', default=False, help='Debug mode')
def serve(host, port, debug):
"""Jalankan development server."""
click.echo(f"Server berjalan di {host}:{port}")
if debug:
click.echo("Debug mode: AKTIF")
# Usage:
# python serve.py --host 0.0.0.0 --port 3000 --debug
# ========================================
# Flag Options (boolean)
# ========================================
@click.command()
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
@click.option('--force', '-f', is_flag=True, help='Force execution')
@click.option('--dry-run', is_flag=True, help='Tanpa eksekusi nyata')
def deploy(verbose, force, dry_run):
"""Deploy aplikasi."""
if dry_run:
click.echo("๐ Mode dry-run, tidak ada eksekusi")
if verbose:
click.echo("Verbose mode aktif")
if force:
click.echo("โ ๏ธ Force mode aktif")
click.echo("Deploy dimulai...")
# ========================================
# Boolean Flag Pair (on/off)
# ========================================
@click.command()
@click.option('--feature/--no-feature', default=False)
def demo(feature):
"""Demo boolean flag pair."""
if feature:
click.echo("Feature AKTIF")
else:
click.echo("Feature NONAKTIF")
# Usage:
# python demo.py --feature โ AKTIF
# python demo.py --no-feature โ NONAKTIF
# python demo.py โ NONAKTIF (default)
# ========================================
# Counting Options
# ========================================
@click.command()
@click.option('--verbose', '-v', count=True, help='Tingkat verbosity')
def verbose_cmd(verbose):
"""Demo counting options."""
click.echo(f"Verbosity level: {verbose}")
# Usage:
# python cmd.py -v โ Level: 1
# python cmd.py -vvv โ Level: 3
# python cmd.py -v -v โ Level: 2
# ========================================
# Multiple Values
# ========================================
@click.command()
@click.option('--tag', '-t', multiple=True, help='Tags (bisa diulang)')
def tag_cmd(tag):
"""Demo multiple values."""
click.echo(f"Tags: {', '.join(tag)}")
# Usage:
# python tag.py -t python -t tutorial -t web
# Tags: python, tutorial, web
# ========================================
# Environment Variable
# ========================================
@click.command()
@click.option('--api-key', envvar='API_KEY', help='API Key (dari $API_KEY)')
@click.option('--debug', envvar='DEBUG', is_flag=True)
def connect(api_key, debug):
"""Connect dengan API key dari environment."""
if api_key:
click.echo(f"API Key: {api_key[:10]}...")
else:
click.echo("API Key tidak ditemukan!")
# Bisa dijalankan dengan:
# API_KEY=mysecret python connect.py
# Atau: python connect.py --api-key mysecret
# ========================================
# Prompt Input
# ========================================
@click.command()
@click.option('--name', prompt=True, help='Nama (akan diprompt jika tidak ada)')
@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True)
def register(name, password):
"""Registrasi user baru."""
click.echo(f"User {name} berhasil didaftarkan!")
# Usage: python register.py
# Nama: Budi
# Password: ****
# Repeat for confirmation: ****
# User Budi berhasil didaftarkan!
# ========================================
# Required Options
# ========================================
@click.command()
@click.option('--name', required=True, help='Nama wajib diisi')
def required_demo(name):
"""Demo option wajib."""
click.echo(f"Name: {name}")
# Usage: python required.py --name Budi
# Tanpa --name โ Error!
6. Prompts dan Konfirmasi
Click menyediakan fungsi bawaan untuk interaksi dengan user melalui terminal โ input teks, konfirmasi, pilihan, dan password.
import click
@click.command()
def setup():
"""Wizard setup interaktif."""
# ========================================
# click.prompt() โ Input dari user
# ========================================
name = click.prompt("Masukkan nama", default="User")
click.echo(f"Nama: {name}")
# Dengan tipe data
age = click.prompt("Masukkan umur", type=int)
click.echo(f"Umur: {age}")
# Dengan custom prompt text
email = click.prompt(
"๐ง Alamat email",
type=str,
prompt_suffix=": "
)
# Default value
city = click.prompt("Kota", default="Jakarta")
# ========================================
# click.confirm() โ Konfirmasi Ya/Tidak
# ========================================
if click.confirm("Apakah Anda ingin melanjutkan?"):
click.echo("Melanjutkan...")
else:
click.echo("Dibatalkan.")
return
# Dengan default
if click.confirm("Hapus file lama?", default=False):
click.echo("File lama dihapus")
# ========================================
# click.echo() โ Output dengan styling
# ========================================
click.echo("Pesan biasa")
click.echo("โ
Berhasil!", color='green')
click.echo("โ ๏ธ Peringatan!", color='yellow')
click.echo("โ Error!", color='red')
# ========================================
# click.secho() โ Styled echo
# ========================================
click.secho("Berhasil!", fg='green', bold=True)
click.secho("Warning!", fg='yellow', bg='black')
click.secho("Error!", fg='red', bold=True)
# ========================================
# click.style() โ Apply styling
# ========================================
styled = click.style("PENTING", fg='red', bold=True, underline=True)
click.echo(f"Perhatian: {styled} - baca instruksi!")
# ========================================
# Progress Bar
# ========================================
import time
total = 100
with click.progressbar(range(total), label='Memproses') as bar:
for i in bar:
time.sleep(0.02) # Simulasi kerja
click.secho("โ
Semua selesai!", fg='green')
# ========================================
# Spinner (dengan library tambahan)
# ========================================
# pip install click-spinner
# import click_spinner
# with click_spinner.spinner():
# time.sleep(5) # Long running task
if __name__ == '__main__':
setup()
7. Command Groups
Groups memungkinkan Anda mengorganisir beberapa commands dalam satu CLI tool. Seperti git yang punya git add, git commit, git push.
import click
# ========================================
# Basic Group
# ========================================
@click.group()
@click.version_option(version='1.0.0', prog_name='mycli')
def cli():
"""MyCLI - Tool manajemen project serba bisa.
Contoh penggunaan:
mycli init --name myproject
mycli build --release
mycli db migrate
"""
pass
# ========================================
# Tambahkan command ke group
# ========================================
@cli.command()
@click.option('--name', '-n', required=True, help='Nama project')
@click.option('--template', '-t', default='basic', help='Template yang digunakan')
@click.option('--git/--no-git', default=True, help='Inisialisasi git')
def init(name, template, git):
"""Inisialisasi project baru."""
click.echo(f"๐ Membuat project: {name}")
click.echo(f" Template: {template}")
if git:
click.echo(" Git: Diinisialisasi")
click.secho(f"โ
Project {name} berhasil dibuat!", fg='green')
@cli.command()
@click.option('--release/--debug', default=False, help='Build mode')
@click.option('--output', '-o', type=click.Path(), default='dist/')
def build(release, output):
"""Build project."""
mode = "Release" if release else "Debug"
click.echo(f"๐จ Building ({mode})...")
click.echo(f" Output: {output}")
click.secho("โ
Build berhasil!", fg='green')
@cli.command()
@click.argument('action', type=click.Choice(['start', 'stop', 'restart']))
@click.option('--port', '-p', default=8000, type=int)
def server(action, port):
"""Kelola development server."""
click.echo(f"๐ฅ๏ธ Server {action} pada port {port}")
@cli.command()
@click.option('--clean', is_flag=True, help='Bersihkan cache')
def test(clean):
"""Jalankan tests."""
if clean:
click.echo("๐งน Membersihkan cache...")
click.echo("๐งช Menjalankan tests...")
click.secho("โ
Semua tests lulus!", fg='green')
# ========================================
# Nested Groups (Group dalam Group)
# ========================================
@cli.group()
def db():
"""Kelola database."""
pass
@db.command()
@click.option('--message', '-m', required=True, help='Pesan migrasi')
def migrate(message):
"""Buat migrasi baru."""
click.echo(f"๐ Migrasi baru: {message}")
@db.command()
def upgrade():
"""Terapkan migrasi."""
click.echo("โฌ๏ธ Menerapkan migrasi...")
click.secho("โ
Database terbaru!", fg='green')
@db.command()
def downgrade():
"""Rollback migrasi."""
click.echo("โฌ๏ธ Rollback migrasi...")
@db.command()
def seed():
"""Seed database dengan data awal."""
click.echo("๐ฑ Seeding database...")
# ========================================
# Shared Context (pass_context)
# ========================================
@cli.group()
@click.option('--config', '-c', default='config.ini', help='Config file')
@click.pass_context
def admin(ctx, config):
"""Panel admin."""
# Simpan config di context agar bisa diakses subcommands
ctx.ensure_object(dict)
ctx.obj['config'] = config
@admin.command()
@click.pass_context
def users(ctx):
"""Kelola users."""
config = ctx.obj['config']
click.echo(f"๐ฅ Kelola users (config: {config})")
@admin.command()
@click.pass_context
def settings(ctx):
"""Kelola settings."""
config = ctx.obj['config']
click.echo(f"โ๏ธ Kelola settings (config: {config})")
# Menjalankan CLI
if __name__ == '__main__':
cli()
# Setelah diinstal (via pip atau setup.py), bisa dijalankan:
# $ mycli --help
# $ mycli init --name myproject
# $ mycli build --release
# $ mycli db migrate -m "add users table"
# $ mycli db upgrade
# $ mycli admin users
# $ mycli --version
8. Error Handling dan Validasi
import click
import sys
# ========================================
# click.ClickException โ Custom Error
# ========================================
@click.command()
@click.argument('filename')
def process(filename):
"""Proses file dengan error handling."""
try:
# Bisa gagal
with open(filename) as f:
content = f.read()
except FileNotFoundError:
# Tampilkan error dengan cara Click
raise click.ClickException(f"File '{filename}' tidak ditemukan!")
if not content.strip():
# click.BadParameter โ error untuk parameter
raise click.BadParameter("File kosong!", param_hint='FILENAME')
click.echo(f"Memproses {len(content)} karakter")
# ========================================
# click.Abort โ Batalkan eksekusi
# ========================================
@click.command()
def dangerous():
"""Operasi berbahaya."""
if not click.confirm("โ ๏ธ Operasi ini berbahaya. Lanjutkan?"):
raise click.Abort()
click.echo("Eksekusi dimulai...")
# ========================================
# Exit Codes
# ========================================
@click.command()
@click.argument('url')
def ping(url):
"""Ping URL."""
click.echo(f"Pinging {url}...")
# Berhasil โ exit code 0 (default)
# Gagal โ exit code non-zero
raise SystemExit(1) # Exit dengan code 1
# ========================================
# Custom Validation
# ========================================
def validate_email(ctx, param, value):
"""Validasi format email."""
if value and '@' not in value:
raise click.BadParameter('Format email tidak valid!')
return value
@click.command()
@click.option('--email', '-e', callback=validate_email, is_eager=True)
def register(email):
"""Registrasi dengan validasi."""
click.echo(f"Email: {email}")
# ========================================
# Custom Type dengan Validation
# ========================================
class EmailParamType(click.ParamType):
name = 'email'
def convert(self, value, param, ctx):
if '@' not in value:
self.fail(f"'{value}' bukan email valid", param, ctx)
if '.' not in value.split('@')[1]:
self.fail(f"Domain email tidak valid", param, ctx)
return value.lower()
EMAIL = EmailParamType()
@click.command()
@click.option('--email', type=EMAIL)
def contact(email):
"""Kontak dengan email valid."""
click.echo(f"Mengirim ke: {email}")
# ========================================
# Format Output
# ========================================
@click.command()
@click.option('--format', '-f', type=click.Choice(['json', 'table', 'csv']),
default='table', help='Format output')
def list_items(format):
"""List items dengan format berbeda."""
items = [
{'name': 'Budi', 'age': 25},
{'name': 'Ani', 'age': 23},
{'name': 'Citra', 'age': 28},
]
if format == 'json':
import json
click.echo(json.dumps(items, indent=2))
elif format == 'table':
click.echo(f"{'Nama':<15} {'Umur':<5}")
click.echo("-" * 20)
for item in items:
click.echo(f"{item['name']:<15} {item['age']:<5}")
elif format == 'csv':
click.echo("name,age")
for item in items:
click.echo(f"{item['name']},{item['age']}")
if __name__ == '__main__':
list_items()
9. Testing CLI Tools
Click menyediakan click.testing.CliRunner untuk menguji CLI tools tanpa menjalankan proses terminal yang sebenarnya.
import click
from click.testing import CliRunner
# ========================================
# CLI yang akan di-test
# ========================================
@click.group()
def cli():
"""App CLI."""
pass
@cli.command()
@click.option('--name', '-n', required=True)
def greet(name):
"""Sapa user."""
click.echo(f"Halo, {name}!")
@cli.command()
@click.argument('filename', type=click.Path())
def count(filename):
"""Hitung baris dalam file."""
try:
with open(filename) as f:
lines = f.readlines()
click.echo(f"{len(lines)} baris")
except FileNotFoundError:
raise click.ClickException(f"File tidak ditemukan: {filename}")
@cli.command()
@click.option('--name', prompt=True)
@click.option('--confirm', is_flag=True)
def create(name, confirm):
"""Buat resource baru."""
click.echo(f"Membuat: {name}")
if confirm:
click.echo("Dikonfirmasi!")
# ========================================
# Test Cases
# ========================================
def test_greet():
"""Test greet command."""
runner = CliRunner()
result = runner.invoke(cli, ['greet', '--name', 'Budi'])
assert result.exit_code == 0
assert 'Halo, Budi!' in result.output
def test_greet_with_flag():
"""Test greet dengan flag."""
runner = CliRunner()
result = runner.invoke(cli, ['greet', '-n', 'Ani'])
assert result.exit_code == 0
assert 'Halo, Ani!' in result.output
def test_count_file():
"""Test count command dengan file."""
runner = CliRunner()
# Buat temporary file
with runner.isolated_filesystem():
with open('test.txt', 'w') as f:
f.write("baris 1\nbaris 2\nbaris 3\n")
result = runner.invoke(cli, ['count', 'test.txt'])
assert result.exit_code == 0
assert '3 baris' in result.output
def test_count_missing_file():
"""Test count dengan file yang tidak ada."""
runner = CliRunner()
result = runner.invoke(cli, ['count', 'nonexistent.txt'])
assert result.exit_code != 0
assert 'tidak ditemukan' in result.output
def test_help():
"""Test help output."""
runner = CliRunner()
result = runner.invoke(cli, ['--help'])
assert result.exit_code == 0
assert 'greet' in result.output
assert 'count' in result.output
def test_create_with_input():
"""Test command dengan input."""
runner = CliRunner()
result = runner.invoke(cli, ['create'], input='TestProject\n')
assert result.exit_code == 0
assert 'Membuat: TestProject' in result.output
# Jalankan tests
# pytest test_cli.py -v
# ========================================
# Runner.isolated_filesystem()
# ========================================
def test_with_isolated_fs():
"""Test dalam isolated filesystem."""
runner = CliRunner()
with runner.isolated_filesystem():
# Buat file di filesystem terisolasi
with open('data.json', 'w') as f:
f.write('{"key": "value"}')
# Operasi di sini tidak mempengaruhi filesystem asli
import os
assert os.path.exists('data.json')
# ========================================
# Mixin Test
# ========================================
def test_greet_error():
"""Test error handling."""
runner = CliRunner()
result = runner.invoke(cli, ['greet']) # Tanpa --name
# Harus error karena --name required
assert result.exit_code != 0
Agar CLI bisa dijalankan langsung dari terminal (tanpa python), gunakan pyproject.toml dengan entry point: [project.scripts] mycli = "myapp.cli:cli". Kemudian instal dengan pip install -e . dan jalankan dengan mycli --help.
Membuat CLI yang Bisa Diinstal
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mycli"
version = "1.0.0"
description = "CLI tool serba bisa"
requires-python = ">=3.8"
dependencies = [
"click>=8.0",
]
[project.scripts]
mycli = "mycli.cli:cli"
# โ Nama command โ Modul:Fungsi
# Setelah "pip install -e .", bisa langsung:
# $ mycli --help
# $ mycli greet --name Budi
10. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Python Click: