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,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/).