Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:04:14 +08:00
commit 70c36b5eff
248 changed files with 47482 additions and 0 deletions

View File

@@ -0,0 +1,405 @@
# Complete Click CLI Example
A production-ready Click CLI demonstrating all major patterns and best practices.
## Full Implementation
```python
#!/usr/bin/env python3
"""
Production-ready Click CLI with all major patterns.
Features:
- Command groups and nested subcommands
- Options and arguments with validation
- Context sharing across commands
- Error handling and colored output
- Configuration management
- Environment-specific commands
"""
import click
from rich.console import Console
from pathlib import Path
import json
console = Console()
# Custom validators
def validate_email(ctx, param, value):
"""Validate email format"""
import re
if value and not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', value):
raise click.BadParameter('Invalid email format')
return value
# Main CLI group
@click.group()
@click.version_option(version='1.0.0')
@click.option('--config', type=click.Path(), default='config.json',
help='Configuration file path')
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
@click.pass_context
def cli(ctx, config, verbose):
"""
A powerful CLI tool for project management.
Examples:
cli init --template basic
cli deploy production --mode safe
cli config get api-key
cli database migrate --create-tables
"""
ctx.ensure_object(dict)
ctx.obj['console'] = console
ctx.obj['verbose'] = verbose
ctx.obj['config_file'] = config
if verbose:
console.print(f"[dim]Config file: {config}[/dim]")
# Initialize command
@cli.command()
@click.option('--template', '-t',
type=click.Choice(['basic', 'advanced', 'minimal']),
default='basic',
help='Project template')
@click.option('--name', prompt=True, help='Project name')
@click.option('--description', prompt=True, help='Project description')
@click.pass_context
def init(ctx, template, name, description):
"""Initialize a new project"""
console = ctx.obj['console']
verbose = ctx.obj['verbose']
console.print(f"[cyan]Initializing project: {name}[/cyan]")
console.print(f"[dim]Template: {template}[/dim]")
console.print(f"[dim]Description: {description}[/dim]")
# Create project structure
project_dir = Path(name)
if project_dir.exists():
console.print(f"[red]✗[/red] Directory already exists: {name}")
raise click.Abort()
try:
project_dir.mkdir(parents=True)
(project_dir / 'src').mkdir()
(project_dir / 'tests').mkdir()
(project_dir / 'docs').mkdir()
# Create config file
config = {
'name': name,
'description': description,
'template': template,
'version': '1.0.0'
}
with open(project_dir / 'config.json', 'w') as f:
json.dump(config, f, indent=2)
console.print(f"[green]✓[/green] Project initialized successfully!")
if verbose:
console.print(f"[dim]Created directories: src/, tests/, docs/[/dim]")
console.print(f"[dim]Created config.json[/dim]")
except Exception as e:
console.print(f"[red]✗[/red] Error: {e}")
raise click.Abort()
# Deploy command
@cli.command()
@click.argument('environment',
type=click.Choice(['dev', 'staging', 'production']))
@click.option('--force', '-f', is_flag=True, help='Force deployment')
@click.option('--mode', '-m',
type=click.Choice(['fast', 'safe', 'rollback']),
default='safe',
help='Deployment mode')
@click.option('--skip-tests', is_flag=True, help='Skip test execution')
@click.pass_context
def deploy(ctx, environment, force, mode, skip_tests):
"""Deploy to specified environment"""
console = ctx.obj['console']
verbose = ctx.obj['verbose']
console.print(f"[cyan]Deploying to {environment} in {mode} mode[/cyan]")
if force:
console.print("[yellow]⚠ Force mode enabled - skipping safety checks[/yellow]")
# Pre-deployment checks
if not skip_tests and not force:
console.print("[dim]Running tests...[/dim]")
# Simulate test execution
if verbose:
console.print("[green]✓[/green] All tests passed")
# Deployment simulation
steps = [
"Building artifacts",
"Uploading to server",
"Running migrations",
"Restarting services",
"Verifying deployment"
]
for step in steps:
console.print(f"[dim]- {step}...[/dim]")
console.print(f"[green]✓[/green] Deployment completed successfully!")
if mode == 'safe':
console.print("[dim]Rollback available for 24 hours[/dim]")
# Config group
@cli.group()
def config():
"""Manage configuration settings"""
pass
@config.command()
@click.argument('key')
@click.pass_context
def get(ctx, key):
"""Get configuration value"""
console = ctx.obj['console']
config_file = ctx.obj['config_file']
try:
if Path(config_file).exists():
with open(config_file) as f:
config_data = json.load(f)
if key in config_data:
console.print(f"[dim]Config[/dim] {key}: [green]{config_data[key]}[/green]")
else:
console.print(f"[yellow]Key not found: {key}[/yellow]")
else:
console.print(f"[red]Config file not found: {config_file}[/red]")
except Exception as e:
console.print(f"[red]Error reading config: {e}[/red]")
@config.command()
@click.argument('key')
@click.argument('value')
@click.pass_context
def set(ctx, key, value):
"""Set configuration value"""
console = ctx.obj['console']
config_file = ctx.obj['config_file']
try:
config_data = {}
if Path(config_file).exists():
with open(config_file) as f:
config_data = json.load(f)
config_data[key] = value
with open(config_file, 'w') as f:
json.dump(config_data, f, indent=2)
console.print(f"[green]✓[/green] Set {key} = {value}")
except Exception as e:
console.print(f"[red]Error writing config: {e}[/red]")
@config.command()
@click.pass_context
def list(ctx):
"""List all configuration settings"""
console = ctx.obj['console']
config_file = ctx.obj['config_file']
try:
if Path(config_file).exists():
with open(config_file) as f:
config_data = json.load(f)
console.print("[cyan]Configuration Settings:[/cyan]")
for key, value in config_data.items():
console.print(f" {key}: [green]{value}[/green]")
else:
console.print("[yellow]No configuration file found[/yellow]")
except Exception as e:
console.print(f"[red]Error reading config: {e}[/red]")
# Database group
@cli.group()
def database():
"""Database management commands"""
pass
@database.command()
@click.option('--create-tables', is_flag=True, help='Create tables')
@click.option('--seed-data', is_flag=True, help='Seed initial data')
@click.pass_context
def migrate(ctx, create_tables, seed_data):
"""Run database migrations"""
console = ctx.obj['console']
console.print("[cyan]Running migrations...[/cyan]")
if create_tables:
console.print("[dim]- Creating tables...[/dim]")
console.print("[green]✓[/green] Tables created")
if seed_data:
console.print("[dim]- Seeding data...[/dim]")
console.print("[green]✓[/green] Data seeded")
console.print("[green]✓[/green] Migrations completed")
@database.command()
@click.option('--confirm', is_flag=True,
prompt='This will delete all data. Continue?',
help='Confirm reset')
@click.pass_context
def reset(ctx, confirm):
"""Reset database (destructive operation)"""
console = ctx.obj['console']
if not confirm:
console.print("[yellow]Operation cancelled[/yellow]")
raise click.Abort()
console.print("[red]Resetting database...[/red]")
console.print("[dim]- Dropping tables...[/dim]")
console.print("[dim]- Clearing cache...[/dim]")
console.print("[green]✓[/green] Database reset completed")
# User management group
@cli.group()
def user():
"""User management commands"""
pass
@user.command()
@click.option('--email', callback=validate_email, prompt=True,
help='User email address')
@click.option('--name', prompt=True, help='User full name')
@click.option('--role',
type=click.Choice(['admin', 'user', 'guest']),
default='user',
help='User role')
@click.pass_context
def create(ctx, email, name, role):
"""Create a new user"""
console = ctx.obj['console']
console.print(f"[cyan]Creating user: {name}[/cyan]")
console.print(f"[dim]Email: {email}[/dim]")
console.print(f"[dim]Role: {role}[/dim]")
console.print(f"[green]✓[/green] User created successfully")
@user.command()
@click.argument('email')
@click.pass_context
def delete(ctx, email):
"""Delete a user"""
console = ctx.obj['console']
if not click.confirm(f"Delete user {email}?"):
console.print("[yellow]Operation cancelled[/yellow]")
return
console.print(f"[cyan]Deleting user: {email}[/cyan]")
console.print(f"[green]✓[/green] User deleted")
# Error handling wrapper
def main():
"""Main entry point with error handling"""
try:
cli(obj={})
except click.Abort:
console.print("[yellow]Operation aborted[/yellow]")
except Exception as e:
console.print(f"[red]Unexpected error: {e}[/red]")
raise
if __name__ == '__main__':
main()
```
## Usage Examples
### Initialize a project
```bash
python cli.py init --template advanced --name myproject --description "My awesome project"
```
### Deploy to production
```bash
python cli.py deploy production --mode safe
python cli.py deploy staging --force --skip-tests
```
### Configuration management
```bash
python cli.py config set api-key abc123
python cli.py config get api-key
python cli.py config list
```
### Database operations
```bash
python cli.py database migrate --create-tables --seed-data
python cli.py database reset --confirm
```
### User management
```bash
python cli.py user create --email user@example.com --name "John Doe" --role admin
python cli.py user delete user@example.com
```
### With verbose output
```bash
python cli.py --verbose deploy production
```
### With custom config file
```bash
python cli.py --config /path/to/config.json config list
```
## Key Features Demonstrated
1. **Command Groups**: Organized commands into logical groups (config, database, user)
2. **Context Sharing**: Using @click.pass_context to share state
3. **Input Validation**: Custom validators for email, built-in validators for choices
4. **Colored Output**: Using Rich console for beautiful output
5. **Error Handling**: Graceful error handling and user feedback
6. **Interactive Prompts**: Using prompt=True for interactive input
7. **Confirmation Dialogs**: Using click.confirm() for dangerous operations
8. **File Operations**: Reading/writing JSON configuration files
9. **Flags and Options**: Boolean flags, default values, short flags
10. **Version Information**: @click.version_option() decorator
## Best Practices Applied
- Clear help text for all commands and options
- Sensible defaults for options
- Validation for user inputs
- Colored output for better UX
- Verbose mode for debugging
- Confirmation for destructive operations
- Proper error handling and messages
- Clean separation of concerns with command groups
- Context object for sharing state

View File

@@ -0,0 +1,482 @@
# Click Framework Edge Cases and Solutions
Common edge cases, gotchas, and their solutions when working with Click.
## Table of Contents
1. [Parameter Handling Edge Cases](#parameter-handling-edge-cases)
2. [Context and State Edge Cases](#context-and-state-edge-cases)
3. [Error Handling Edge Cases](#error-handling-edge-cases)
4. [Testing Edge Cases](#testing-edge-cases)
5. [Platform-Specific Edge Cases](#platform-specific-edge-cases)
---
## Parameter Handling Edge Cases
### Case 1: Multiple Values with Same Option
**Problem**: User specifies the same option multiple times
```bash
cli --tag python --tag docker --tag kubernetes
```
**Solution**: Use `multiple=True`
```python
@click.option('--tag', multiple=True)
def command(tag):
"""Handle multiple values"""
for t in tag:
click.echo(t)
```
### Case 2: Option vs Argument Ambiguity
**Problem**: Argument that looks like an option
```bash
cli process --file=-myfile.txt # -myfile.txt looks like option
```
**Solution**: Use `--` separator or quotes
```python
@click.command()
@click.argument('filename')
def process(filename):
pass
# Usage:
# cli process -- -myfile.txt
# cli process "-myfile.txt"
```
### Case 3: Empty String vs None
**Problem**: Distinguishing between no value and empty string
```python
@click.option('--name')
def command(name):
# name is None when not provided
# name is '' when provided as empty
if name is None:
click.echo('Not provided')
elif name == '':
click.echo('Empty string provided')
```
**Solution**: Use callback for custom handling
```python
def handle_empty(ctx, param, value):
if value == '':
return None # Treat empty as None
return value
@click.option('--name', callback=handle_empty)
def command(name):
pass
```
### Case 4: Boolean Flag with Default True
**Problem**: Need a flag that's True by default, but can be disabled
```python
# Wrong approach:
@click.option('--enable', is_flag=True, default=True) # Doesn't work as expected
# Correct approach:
@click.option('--disable', is_flag=True)
def command(disable):
enabled = not disable
```
**Better Solution**: Use flag_value
```python
@click.option('--ssl/--no-ssl', default=True)
def command(ssl):
"""SSL is enabled by default, use --no-ssl to disable"""
pass
```
### Case 5: Required Option with Environment Variable
**Problem**: Make option required unless env var is set
```python
def require_if_no_env(ctx, param, value):
"""Require option if environment variable not set"""
if value is None:
import os
env_value = os.getenv('API_KEY')
if env_value:
return env_value
raise click.MissingParameter(param=param)
return value
@click.option('--api-key', callback=require_if_no_env)
def command(api_key):
pass
```
---
## Context and State Edge Cases
### Case 6: Context Not Available in Callbacks
**Problem**: Need context in parameter callback
```python
# This doesn't work - context not yet initialized:
def my_callback(ctx, param, value):
config = ctx.obj['config'] # Error: ctx.obj is None
return value
```
**Solution**: Use command decorator to set up context first
```python
@click.command()
@click.pass_context
def cli(ctx):
ctx.ensure_object(dict)
ctx.obj['config'] = load_config()
@cli.command()
@click.option('--value', callback=validate_with_config)
@click.pass_context
def subcommand(ctx, value):
# Now ctx.obj is available
pass
```
### Case 7: Sharing State Between Command Groups
**Problem**: State not persisting across nested groups
```python
@click.group()
@click.pass_context
def cli(ctx):
ctx.ensure_object(dict)
ctx.obj['data'] = 'test'
@cli.group()
@click.pass_context
def subgroup(ctx):
# ctx.obj is still available here
assert ctx.obj['data'] == 'test'
@subgroup.command()
@click.pass_context
def command(ctx):
# ctx.obj is still available here too
assert ctx.obj['data'] == 'test'
```
### Case 8: Mutating Context Objects
**Problem**: Changes to context not persisting
```python
# This works:
ctx.obj['key'] = 'value' # Modifying dict
# This doesn't persist:
ctx.obj = {'key': 'value'} # Replacing dict
```
---
## Error Handling Edge Cases
### Case 9: Graceful Handling of Ctrl+C
**Problem**: Ugly traceback on keyboard interrupt
```python
def main():
try:
cli()
except KeyboardInterrupt:
click.echo('\n\nOperation cancelled by user')
raise SystemExit(130) # Standard exit code for Ctrl+C
if __name__ == '__main__':
main()
```
### Case 10: Custom Error Messages for Validation
**Problem**: Default error messages aren't user-friendly
```python
# Default error:
@click.option('--port', type=click.IntRange(1, 65535))
# Error: Invalid value for '--port': 70000 is not in the range 1<=x<=65535
# Custom error:
def validate_port(ctx, param, value):
if not 1 <= value <= 65535:
raise click.BadParameter(
f'Port {value} is out of range. Please use a port between 1 and 65535.'
)
return value
@click.option('--port', type=int, callback=validate_port)
```
### Case 11: Handling Mutually Exclusive Options
**Problem**: Options that can't be used together
```python
def validate_exclusive(ctx, param, value):
"""Ensure mutually exclusive options"""
if value and ctx.params.get('other_option'):
raise click.UsageError(
'Cannot use --option and --other-option together'
)
return value
@click.command()
@click.option('--option', callback=validate_exclusive)
@click.option('--other-option')
def command(option, other_option):
pass
```
### Case 12: Dependent Options
**Problem**: One option requires another
```python
@click.command()
@click.option('--ssl', is_flag=True)
@click.option('--cert', type=click.Path(exists=True))
@click.option('--key', type=click.Path(exists=True))
def server(ssl, cert, key):
"""Validate dependent options"""
if ssl:
if not cert or not key:
raise click.UsageError(
'--ssl requires both --cert and --key'
)
```
---
## Testing Edge Cases
### Case 13: Testing with Environment Variables
**Problem**: Tests failing due to environment pollution
```python
def test_with_clean_env():
"""Test with isolated environment"""
runner = CliRunner()
# This isolates environment variables:
result = runner.invoke(
cli,
['command'],
env={'API_KEY': 'test'},
catch_exceptions=False
)
assert result.exit_code == 0
```
### Case 14: Testing Interactive Prompts with Validation
**Problem**: Prompts with retry logic
```python
def test_interactive_retry():
"""Test prompt with retry on invalid input"""
runner = CliRunner()
# Provide multiple inputs (first invalid, second valid)
result = runner.invoke(
cli,
['create'],
input='invalid-email\nvalid@email.com\n'
)
assert 'Invalid email' in result.output
assert result.exit_code == 0
```
### Case 15: Testing File Operations
**Problem**: Tests creating actual files
```python
def test_file_operations():
"""Test with isolated filesystem"""
runner = CliRunner()
with runner.isolated_filesystem():
# Create test file
with open('input.txt', 'w') as f:
f.write('test data')
result = runner.invoke(cli, ['process', 'input.txt'])
# Verify output file
assert Path('output.txt').exists()
```
---
## Platform-Specific Edge Cases
### Case 16: Windows Path Handling
**Problem**: Backslashes in Windows paths
```python
@click.option('--path', type=click.Path())
def command(path):
# Use pathlib for cross-platform compatibility
from pathlib import Path
p = Path(path) # Handles Windows/Unix paths
```
### Case 17: Unicode in Command Line Arguments
**Problem**: Non-ASCII characters in arguments
```python
@click.command()
@click.argument('name')
def greet(name):
"""Handle unicode properly"""
# Click handles unicode automatically on Python 3
click.echo(f'Hello, {name}!')
# This works:
# cli greet "José"
# cli greet "北京"
```
### Case 18: Terminal Width Detection
**Problem**: Output formatting for different terminal sizes
```python
@click.command()
def status():
"""Adapt to terminal width"""
terminal_width = click.get_terminal_size()[0]
if terminal_width < 80:
# Compact output for narrow terminals
click.echo('Status: OK')
else:
# Detailed output for wide terminals
click.echo('=' * terminal_width)
click.echo('Detailed Status Information')
click.echo('=' * terminal_width)
```
---
## Advanced Edge Cases
### Case 19: Dynamic Command Registration
**Problem**: Register commands at runtime
```python
class DynamicGroup(click.Group):
"""Group that discovers commands dynamically"""
def list_commands(self, ctx):
"""List available commands"""
# Dynamically discover commands
return ['cmd1', 'cmd2', 'cmd3']
def get_command(self, ctx, name):
"""Load command on demand"""
if name in self.list_commands(ctx):
# Import and return command
module = __import__(f'commands.{name}')
return getattr(module, name).cli
return None
@click.command(cls=DynamicGroup)
def cli():
pass
```
### Case 20: Command Aliases
**Problem**: Support command aliases
```python
class AliasedGroup(click.Group):
"""Group that supports command aliases"""
def get_command(self, ctx, cmd_name):
"""Resolve aliases"""
aliases = {
'ls': 'list',
'rm': 'remove',
'cp': 'copy'
}
# Resolve alias
resolved = aliases.get(cmd_name, cmd_name)
return super().get_command(ctx, resolved)
@click.group(cls=AliasedGroup)
def cli():
pass
@cli.command()
def list():
"""List items (alias: ls)"""
pass
```
### Case 21: Progress Bar with Unknown Length
**Problem**: Show progress when total is unknown
```python
@click.command()
def process():
"""Process with indeterminate progress"""
import time
# For unknown length, use length=None
with click.progressbar(
range(100),
length=None,
label='Processing'
) as bar:
for _ in bar:
time.sleep(0.1)
```
---
## Summary
Key takeaways for handling edge cases:
1. **Parameters**: Use callbacks and custom types for complex validation
2. **Context**: Ensure context is initialized before accessing ctx.obj
3. **Errors**: Provide clear, actionable error messages
4. **Testing**: Use CliRunner's isolation features
5. **Platform**: Use pathlib and Click's built-in utilities for portability
For more edge cases, consult the [Click documentation](https://click.palletsprojects.com/) and [GitHub issues](https://github.com/pallets/click/issues).

View File

@@ -0,0 +1,521 @@
# Click Framework Common Patterns
Best practices and common patterns for building production-ready Click CLIs.
## Table of Contents
1. [Command Structure Patterns](#command-structure-patterns)
2. [Parameter Patterns](#parameter-patterns)
3. [Validation Patterns](#validation-patterns)
4. [Error Handling Patterns](#error-handling-patterns)
5. [Output Patterns](#output-patterns)
6. [Configuration Patterns](#configuration-patterns)
7. [Testing Patterns](#testing-patterns)
---
## Command Structure Patterns
### Single Command CLI
For simple tools with one main function:
```python
@click.command()
@click.option('--name', default='World')
def hello(name):
"""Simple greeting CLI"""
click.echo(f'Hello, {name}!')
```
### Command Group Pattern
For CLIs with multiple related commands:
```python
@click.group()
def cli():
"""Main CLI entry point"""
pass
@cli.command()
def cmd1():
"""First command"""
pass
@cli.command()
def cmd2():
"""Second command"""
pass
```
### Nested Command Groups
For complex CLIs with logical grouping:
```python
@click.group()
def cli():
"""Main CLI"""
pass
@cli.group()
def database():
"""Database commands"""
pass
@database.command()
def migrate():
"""Run migrations"""
pass
@database.command()
def reset():
"""Reset database"""
pass
```
### Context-Aware Commands
Share state across commands:
```python
@click.group()
@click.pass_context
def cli(ctx):
"""Main CLI with shared context"""
ctx.ensure_object(dict)
ctx.obj['config'] = load_config()
@cli.command()
@click.pass_context
def deploy(ctx):
"""Use shared config"""
config = ctx.obj['config']
```
---
## Parameter Patterns
### Options vs Arguments
**Options** (optional, named):
```python
@click.option('--name', '-n', default='World', help='Name to greet')
@click.option('--count', '-c', default=1, type=int)
```
**Arguments** (required, positional):
```python
@click.argument('filename')
@click.argument('output', type=click.Path())
```
### Required Options
```python
@click.option('--api-key', required=True, help='API key (required)')
@click.option('--config', required=True, type=click.Path(exists=True))
```
### Multiple Values
```python
# Multiple option values
@click.option('--tag', multiple=True, help='Tags (can specify multiple times)')
def command(tag):
for t in tag:
click.echo(t)
# Variable arguments
@click.argument('files', nargs=-1, type=click.Path(exists=True))
def process(files):
for f in files:
click.echo(f)
```
### Environment Variables
```python
@click.option('--api-key', envvar='API_KEY', help='API key (from env: API_KEY)')
@click.option('--debug', envvar='DEBUG', is_flag=True)
```
### Interactive Prompts
```python
@click.option('--name', prompt=True, help='Your name')
@click.option('--password', prompt=True, hide_input=True)
@click.option('--confirm', prompt='Continue?', confirmation_prompt=True)
```
---
## Validation Patterns
### Built-in Validators
```python
# Choice validation
@click.option('--env', type=click.Choice(['dev', 'staging', 'prod']))
# Range validation
@click.option('--port', type=click.IntRange(1, 65535))
@click.option('--rate', type=click.FloatRange(0.0, 1.0))
# Path validation
@click.option('--input', type=click.Path(exists=True, dir_okay=False))
@click.option('--output', type=click.Path(writable=True))
@click.option('--dir', type=click.Path(exists=True, file_okay=False))
```
### Custom Validators with Callbacks
```python
def validate_email(ctx, param, value):
"""Validate email format"""
import re
if value and not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', value):
raise click.BadParameter('Invalid email format')
return value
@click.option('--email', callback=validate_email)
def command(email):
pass
```
### Custom Click Types
```python
class EmailType(click.ParamType):
"""Custom email type"""
name = 'email'
def convert(self, value, param, ctx):
import re
if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', value):
self.fail(f'{value} is not a valid email', param, ctx)
return value
@click.option('--email', type=EmailType())
def command(email):
pass
```
### Conditional Validation
```python
@click.command()
@click.option('--ssl', is_flag=True)
@click.option('--cert', type=click.Path(exists=True))
@click.option('--key', type=click.Path(exists=True))
def server(ssl, cert, key):
"""Start server with SSL validation"""
if ssl and (not cert or not key):
raise click.UsageError('SSL requires --cert and --key')
```
---
## Error Handling Patterns
### Graceful Error Handling
```python
@click.command()
def command():
"""Command with error handling"""
try:
# Operation that might fail
result = risky_operation()
except FileNotFoundError as e:
raise click.FileError(str(e))
except Exception as e:
raise click.ClickException(f'Operation failed: {e}')
```
### Custom Exit Codes
```python
@click.command()
def deploy():
"""Deploy with custom exit codes"""
if not check_prerequisites():
ctx = click.get_current_context()
ctx.exit(1)
if not deploy_application():
ctx = click.get_current_context()
ctx.exit(2)
click.echo('Deployment successful')
ctx = click.get_current_context()
ctx.exit(0)
```
### Confirmation for Dangerous Operations
```python
@click.command()
@click.option('--force', is_flag=True, help='Skip confirmation')
def delete(force):
"""Delete with confirmation"""
if not force:
click.confirm('This will delete all data. Continue?', abort=True)
# Proceed with deletion
click.echo('Deleting...')
```
---
## Output Patterns
### Colored Output with Click
```python
@click.command()
def status():
"""Show status with colors"""
click.secho('Success!', fg='green', bold=True)
click.secho('Warning!', fg='yellow')
click.secho('Error!', fg='red', bold=True)
click.echo(click.style('Info', fg='cyan'))
```
### Rich Console Integration
```python
from rich.console import Console
console = Console()
@click.command()
def deploy():
"""Deploy with Rich output"""
console.print('[cyan]Starting deployment...[/cyan]')
console.print('[green]✓[/green] Build successful')
console.print('[yellow]⚠[/yellow] Warning: Cache cleared')
```
### Progress Bars
```python
@click.command()
@click.argument('files', nargs=-1)
def process(files):
"""Process with progress bar"""
with click.progressbar(files, label='Processing files') as bar:
for file in bar:
# Process file
time.sleep(0.1)
```
### Verbose Mode Pattern
```python
@click.command()
@click.option('--verbose', '-v', is_flag=True)
def command(verbose):
"""Command with verbose output"""
click.echo('Starting operation...')
if verbose:
click.echo('Debug: Loading configuration')
click.echo('Debug: Connecting to database')
# Main operation
click.echo('Operation completed')
```
---
## Configuration Patterns
### Configuration File Loading
```python
import json
from pathlib import Path
@click.group()
@click.option('--config', type=click.Path(), default='config.json')
@click.pass_context
def cli(ctx, config):
"""CLI with config file"""
ctx.ensure_object(dict)
if Path(config).exists():
with open(config) as f:
ctx.obj['config'] = json.load(f)
else:
ctx.obj['config'] = {}
@cli.command()
@click.pass_context
def deploy(ctx):
"""Use config"""
config = ctx.obj['config']
api_key = config.get('api_key')
```
### Environment-Based Configuration
```python
@click.command()
@click.option('--env', type=click.Choice(['dev', 'staging', 'prod']), default='dev')
def deploy(env):
"""Deploy with environment config"""
config_file = f'config.{env}.json'
with open(config_file) as f:
config = json.load(f)
# Use environment-specific config
```
### Configuration Priority
```python
def get_config_value(ctx, param_value, env_var, config_key, default):
"""Get value with priority: param > env > config > default"""
if param_value:
return param_value
if env_var in os.environ:
return os.environ[env_var]
if config_key in ctx.obj['config']:
return ctx.obj['config'][config_key]
return default
```
---
## Testing Patterns
### Basic Testing with CliRunner
```python
from click.testing import CliRunner
import pytest
def test_command():
"""Test Click command"""
runner = CliRunner()
result = runner.invoke(cli, ['--help'])
assert result.exit_code == 0
assert 'Usage:' in result.output
def test_command_with_args():
"""Test with arguments"""
runner = CliRunner()
result = runner.invoke(cli, ['deploy', 'production'])
assert result.exit_code == 0
assert 'Deploying to production' in result.output
```
### Testing with Temporary Files
```python
def test_with_file():
"""Test with temporary file"""
runner = CliRunner()
with runner.isolated_filesystem():
with open('test.txt', 'w') as f:
f.write('test content')
result = runner.invoke(cli, ['process', 'test.txt'])
assert result.exit_code == 0
```
### Testing Interactive Prompts
```python
def test_interactive():
"""Test interactive prompts"""
runner = CliRunner()
result = runner.invoke(cli, ['create'], input='username\npassword\n')
assert result.exit_code == 0
assert 'User created' in result.output
```
### Testing Environment Variables
```python
def test_with_env():
"""Test with environment variables"""
runner = CliRunner()
result = runner.invoke(cli, ['deploy'], env={'API_KEY': 'test123'})
assert result.exit_code == 0
```
---
## Advanced Patterns
### Plugin System
```python
@click.group()
def cli():
"""CLI with plugin support"""
pass
# Allow plugins to register commands
def register_plugin(group, plugin_name):
"""Register plugin commands"""
plugin_module = importlib.import_module(f'plugins.{plugin_name}')
for name, cmd in plugin_module.commands.items():
group.add_command(cmd, name)
```
### Lazy Loading
```python
class LazyGroup(click.Group):
"""Lazy load commands"""
def get_command(self, ctx, cmd_name):
"""Load command on demand"""
module = importlib.import_module(f'commands.{cmd_name}')
return module.cli
@click.command(cls=LazyGroup)
def cli():
"""CLI with lazy loading"""
pass
```
### Middleware Pattern
```python
def with_database(f):
"""Decorator to inject database connection"""
@click.pass_context
def wrapper(ctx, *args, **kwargs):
ctx.obj['db'] = connect_database()
try:
return f(*args, **kwargs)
finally:
ctx.obj['db'].close()
return wrapper
@cli.command()
@with_database
@click.pass_context
def query(ctx):
"""Command with database"""
db = ctx.obj['db']
```
---
## Summary
These patterns cover the most common use cases for Click CLIs:
1. **Structure**: Choose between single command, command group, or nested groups
2. **Parameters**: Use options for named parameters, arguments for positional
3. **Validation**: Leverage built-in validators or create custom ones
4. **Errors**: Handle errors gracefully with proper messages
5. **Output**: Use colored output and progress bars for better UX
6. **Config**: Load configuration from files with proper priority
7. **Testing**: Test thoroughly with CliRunner
For more patterns and advanced usage, see the [Click documentation](https://click.palletsprojects.com/).