Python

Python Click: Build CLI Tools Lengkap

Tutorial lengkap Python Click โ€” membuat command line tools profesional dengan commands, arguments, options, prompts, groups, validation, dan testing

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)
PendekatanDekoratorParser 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

Diagram: Click Architecture
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  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

Terminal
# Instal Click
pip install click

# Verifikasi
python -c "import click; print(click.__version__)"
# Output: 8.x.x

CLI Pertama

Python โ€” hello_cli.py
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.

Python โ€” Commands
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 --.

Python โ€” Arguments
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 biasaclick.argument('name')
intIntegerclick.argument('n', type=int)
floatFloatclick.argument('x', type=float)
click.Path()File/directory pathclick.argument('f', type=click.Path())
click.File()Auto-open fileclick.argument('f', type=click.File('r'))
click.Choice()Pilihan terbatasclick.argument('fmt', type=click.Choice(['a','b']))
click.IntRange()Integer dengan rangeclick.argument('n', type=click.IntRange(0,100))
nargs=-1Variadic (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.

Python โ€” Options
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.

Python โ€” Prompts
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.

Python โ€” Command Groups
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

Python โ€” Error Handling
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.

Python โ€” Testing CLI
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
๐Ÿ’ก Tips: Membuat CLI Installable

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

TOML โ€” pyproject.toml
[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:

Pertanyaan 1: Apa perbedaan utama antara Arguments dan Options di Click?

a) Tidak ada perbedaan
b) Arguments positional (wajib), Options pakai --prefix (opsional)
c) Arguments hanya untuk angka, Options untuk teks
d) Options lebih cepat dari Arguments

Pertanyaan 2: Dekorator apa yang digunakan untuk membuat subcommands di Click?

a) @click.command()
b) @click.group()
c) @click.subcommand()
d) @click.parent()

Pertanyaan 3: Bagaimana cara membuat option boolean flag di Click?

a) @click.option('--verbose', type=bool)
b) @click.option('--verbose', is_flag=True)
c) @click.option('--verbose', flag=True)
d) @click.option('--verbose', boolean=True)

Pertanyaan 4: Bagaimana cara menguji CLI tools di Click?

a) Menggunakan subprocess.run()
b) Menggunakan click.testing.CliRunner
c) Menggunakan unittest.mock.patch()
d) Tidak bisa di-test otomatis

Pertanyaan 5: Apa fungsi dari @click.pass_context?

a) Mengambil environment variable
b) Melempar exception ke parent command
c) Meneruskan context object (dengan data shared) ke fungsi command
d) Mengatur konfigurasi logging
๐Ÿ” Zoom
100%
๐ŸŽจ Tema