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