Initial commit
This commit is contained in:
126
skills/click-patterns/SKILL.md
Normal file
126
skills/click-patterns/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: click-patterns
|
||||
description: Click framework examples and templates - decorators, nested commands, parameter validation. Use when building Python CLI with Click, implementing command groups, adding CLI options/arguments, validating CLI parameters, creating nested subcommands, or when user mentions Click framework, @click decorators, command-line interface.
|
||||
allowed-tools: Read, Write, Bash
|
||||
---
|
||||
|
||||
# Click Framework Patterns
|
||||
|
||||
This skill provides comprehensive Click framework patterns, templates, and examples for building production-ready Python CLIs.
|
||||
|
||||
## Instructions
|
||||
|
||||
### When Building a Click CLI
|
||||
|
||||
1. Read the appropriate template based on complexity:
|
||||
- Simple CLI: `templates/basic-cli.py`
|
||||
- Nested commands: `templates/nested-commands.py`
|
||||
- Custom validators: `templates/validators.py`
|
||||
|
||||
2. Generate new Click project:
|
||||
```bash
|
||||
bash scripts/generate-click-cli.sh <project-name> <cli-type>
|
||||
```
|
||||
Where cli-type is: basic, nested, or advanced
|
||||
|
||||
3. Study complete examples:
|
||||
- `examples/complete-example.md` - Full-featured CLI
|
||||
- `examples/patterns.md` - Common patterns and best practices
|
||||
|
||||
4. Validate your Click setup:
|
||||
```bash
|
||||
bash scripts/validate-click.sh <cli-file.py>
|
||||
```
|
||||
|
||||
### Core Click Patterns
|
||||
|
||||
**Command Groups:**
|
||||
```python
|
||||
@click.group()
|
||||
def cli():
|
||||
"""Main CLI entry point"""
|
||||
pass
|
||||
|
||||
@cli.command()
|
||||
def subcommand():
|
||||
"""A subcommand"""
|
||||
pass
|
||||
```
|
||||
|
||||
**Options and Arguments:**
|
||||
```python
|
||||
@click.option('--template', '-t', default='basic', help='Template name')
|
||||
@click.argument('environment')
|
||||
def deploy(template, environment):
|
||||
pass
|
||||
```
|
||||
|
||||
**Nested Groups:**
|
||||
```python
|
||||
@cli.group()
|
||||
def config():
|
||||
"""Configuration management"""
|
||||
pass
|
||||
|
||||
@config.command()
|
||||
def get():
|
||||
"""Get config value"""
|
||||
pass
|
||||
```
|
||||
|
||||
**Parameter Validation:**
|
||||
```python
|
||||
@click.option('--mode', type=click.Choice(['fast', 'safe', 'rollback']))
|
||||
@click.option('--count', type=click.IntRange(1, 100))
|
||||
def command(mode, count):
|
||||
pass
|
||||
```
|
||||
|
||||
### Available Templates
|
||||
|
||||
1. **basic-cli.py** - Simple single-command CLI
|
||||
2. **nested-commands.py** - Command groups and subcommands
|
||||
3. **validators.py** - Custom parameter validators
|
||||
4. **advanced-cli.py** - Advanced patterns with plugins and chaining
|
||||
|
||||
### Available Scripts
|
||||
|
||||
1. **generate-click-cli.sh** - Creates Click project structure
|
||||
2. **validate-click.sh** - Validates Click CLI implementation
|
||||
3. **setup-click-project.sh** - Setup dependencies and environment
|
||||
|
||||
### Available Examples
|
||||
|
||||
1. **complete-example.md** - Production-ready Click CLI
|
||||
2. **patterns.md** - Best practices and common patterns
|
||||
3. **edge-cases.md** - Edge cases and solutions
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- Click 8.0+ (`pip install click`)
|
||||
- Rich for colored output (`pip install rich`)
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use command groups** for organizing related commands
|
||||
2. **Add help text** to all commands and options
|
||||
3. **Validate parameters** using Click's built-in validators
|
||||
4. **Use context** (@click.pass_context) for sharing state
|
||||
5. **Handle errors gracefully** with try-except blocks
|
||||
6. **Add version info** with @click.version_option()
|
||||
7. **Use Rich** for beautiful colored output
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
- Building CLI tools with multiple commands
|
||||
- Creating deployment scripts with options
|
||||
- Implementing configuration management CLIs
|
||||
- Building database migration tools
|
||||
- Creating API testing CLIs
|
||||
- Implementing project scaffolding tools
|
||||
|
||||
---
|
||||
|
||||
**Purpose:** Provide Click framework templates and patterns for Python CLI development
|
||||
**Load when:** Building Click CLIs, implementing command groups, or validating CLI parameters
|
||||
405
skills/click-patterns/examples/complete-example.md
Normal file
405
skills/click-patterns/examples/complete-example.md
Normal 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
|
||||
482
skills/click-patterns/examples/edge-cases.md
Normal file
482
skills/click-patterns/examples/edge-cases.md
Normal 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).
|
||||
521
skills/click-patterns/examples/patterns.md
Normal file
521
skills/click-patterns/examples/patterns.md
Normal 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/).
|
||||
334
skills/click-patterns/scripts/generate-click-cli.sh
Executable file
334
skills/click-patterns/scripts/generate-click-cli.sh
Executable file
@@ -0,0 +1,334 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# generate-click-cli.sh - Generate Click CLI project structure
|
||||
#
|
||||
# Usage: generate-click-cli.sh <project-name> [cli-type]
|
||||
# cli-type: basic, nested, or advanced (default: basic)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_info() { echo -e "${CYAN}ℹ${NC} $1"; }
|
||||
print_success() { echo -e "${GREEN}✓${NC} $1"; }
|
||||
print_error() { echo -e "${RED}✗${NC} $1" >&2; }
|
||||
print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||
|
||||
# Validate arguments
|
||||
if [ $# -lt 1 ]; then
|
||||
print_error "Usage: $0 <project-name> [cli-type]"
|
||||
echo " cli-type: basic, nested, or advanced (default: basic)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROJECT_NAME="$1"
|
||||
CLI_TYPE="${2:-basic}"
|
||||
|
||||
# Validate CLI type
|
||||
if [[ ! "$CLI_TYPE" =~ ^(basic|nested|advanced)$ ]]; then
|
||||
print_error "Invalid CLI type: $CLI_TYPE"
|
||||
echo " Valid types: basic, nested, advanced"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate project name
|
||||
if [[ ! "$PROJECT_NAME" =~ ^[a-z0-9_-]+$ ]]; then
|
||||
print_error "Invalid project name: $PROJECT_NAME"
|
||||
echo " Must contain only lowercase letters, numbers, hyphens, and underscores"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create project directory
|
||||
if [ -d "$PROJECT_NAME" ]; then
|
||||
print_error "Directory already exists: $PROJECT_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Creating Click CLI project: $PROJECT_NAME (type: $CLI_TYPE)"
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "$PROJECT_NAME"/{src,tests,docs}
|
||||
|
||||
# Determine which template to use
|
||||
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
TEMPLATE_FILE=""
|
||||
|
||||
case "$CLI_TYPE" in
|
||||
basic)
|
||||
TEMPLATE_FILE="$SKILL_DIR/templates/basic-cli.py"
|
||||
;;
|
||||
nested)
|
||||
TEMPLATE_FILE="$SKILL_DIR/templates/nested-commands.py"
|
||||
;;
|
||||
advanced)
|
||||
# For advanced, use nested as base with validators
|
||||
TEMPLATE_FILE="$SKILL_DIR/templates/nested-commands.py"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Copy template
|
||||
if [ ! -f "$TEMPLATE_FILE" ]; then
|
||||
print_error "Template file not found: $TEMPLATE_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$TEMPLATE_FILE" "$PROJECT_NAME/src/cli.py"
|
||||
print_success "Created src/cli.py from template"
|
||||
|
||||
# Copy validators if advanced type
|
||||
if [ "$CLI_TYPE" = "advanced" ]; then
|
||||
VALIDATORS_FILE="$SKILL_DIR/templates/validators.py"
|
||||
if [ -f "$VALIDATORS_FILE" ]; then
|
||||
cp "$VALIDATORS_FILE" "$PROJECT_NAME/src/validators.py"
|
||||
print_success "Created src/validators.py"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create __init__.py
|
||||
cat > "$PROJECT_NAME/src/__init__.py" <<'EOF'
|
||||
"""
|
||||
CLI application package
|
||||
"""
|
||||
from .cli import cli
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__all__ = ["cli"]
|
||||
EOF
|
||||
print_success "Created src/__init__.py"
|
||||
|
||||
# Create requirements.txt
|
||||
cat > "$PROJECT_NAME/requirements.txt" <<'EOF'
|
||||
click>=8.0.0
|
||||
rich>=13.0.0
|
||||
EOF
|
||||
print_success "Created requirements.txt"
|
||||
|
||||
# Create setup.py
|
||||
cat > "$PROJECT_NAME/setup.py" <<EOF
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="${PROJECT_NAME}",
|
||||
version="1.0.0",
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
"click>=8.0.0",
|
||||
"rich>=13.0.0",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"${PROJECT_NAME}=src.cli:cli",
|
||||
],
|
||||
},
|
||||
python_requires=">=3.8",
|
||||
)
|
||||
EOF
|
||||
print_success "Created setup.py"
|
||||
|
||||
# Create pyproject.toml
|
||||
cat > "$PROJECT_NAME/pyproject.toml" <<EOF
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "${PROJECT_NAME}"
|
||||
version = "1.0.0"
|
||||
description = "A Click-based CLI tool"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"click>=8.0.0",
|
||||
"rich>=13.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
${PROJECT_NAME} = "src.cli:cli"
|
||||
EOF
|
||||
print_success "Created pyproject.toml"
|
||||
|
||||
# Create README.md
|
||||
cat > "$PROJECT_NAME/README.md" <<EOF
|
||||
# ${PROJECT_NAME}
|
||||
|
||||
A CLI tool built with Click framework.
|
||||
|
||||
## Installation
|
||||
|
||||
\`\`\`bash
|
||||
pip install -e .
|
||||
\`\`\`
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`bash
|
||||
# Show help
|
||||
${PROJECT_NAME} --help
|
||||
|
||||
# Run command
|
||||
${PROJECT_NAME} <command>
|
||||
\`\`\`
|
||||
|
||||
## Development
|
||||
|
||||
\`\`\`bash
|
||||
# Install in development mode
|
||||
pip install -e .
|
||||
|
||||
# Run tests
|
||||
pytest tests/
|
||||
|
||||
# Format code
|
||||
black src/ tests/
|
||||
|
||||
# Lint code
|
||||
pylint src/ tests/
|
||||
\`\`\`
|
||||
|
||||
## Project Structure
|
||||
|
||||
\`\`\`
|
||||
${PROJECT_NAME}/
|
||||
├── src/
|
||||
│ ├── __init__.py
|
||||
│ └── cli.py # Main CLI implementation
|
||||
├── tests/
|
||||
│ └── test_cli.py # Unit tests
|
||||
├── docs/
|
||||
│ └── usage.md # Usage documentation
|
||||
├── requirements.txt # Dependencies
|
||||
├── setup.py # Setup configuration
|
||||
└── README.md # This file
|
||||
\`\`\`
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
EOF
|
||||
print_success "Created README.md"
|
||||
|
||||
# Create basic test file
|
||||
cat > "$PROJECT_NAME/tests/test_cli.py" <<'EOF'
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from src.cli import cli
|
||||
|
||||
|
||||
def test_cli_help():
|
||||
"""Test CLI help output"""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['--help'])
|
||||
assert result.exit_code == 0
|
||||
assert 'Usage:' in result.output
|
||||
|
||||
|
||||
def test_cli_version():
|
||||
"""Test CLI version output"""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['--version'])
|
||||
assert result.exit_code == 0
|
||||
assert '1.0.0' in result.output
|
||||
EOF
|
||||
print_success "Created tests/test_cli.py"
|
||||
|
||||
# Create .gitignore
|
||||
cat > "$PROJECT_NAME/.gitignore" <<'EOF'
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
EOF
|
||||
print_success "Created .gitignore"
|
||||
|
||||
# Create usage documentation
|
||||
cat > "$PROJECT_NAME/docs/usage.md" <<EOF
|
||||
# ${PROJECT_NAME} Usage Guide
|
||||
|
||||
## Installation
|
||||
|
||||
Install the CLI tool:
|
||||
|
||||
\`\`\`bash
|
||||
pip install -e .
|
||||
\`\`\`
|
||||
|
||||
## Commands
|
||||
|
||||
### Help
|
||||
|
||||
Show available commands:
|
||||
|
||||
\`\`\`bash
|
||||
${PROJECT_NAME} --help
|
||||
\`\`\`
|
||||
|
||||
### Version
|
||||
|
||||
Show version information:
|
||||
|
||||
\`\`\`bash
|
||||
${PROJECT_NAME} --version
|
||||
\`\`\`
|
||||
|
||||
## Examples
|
||||
|
||||
Add specific examples for your CLI commands here.
|
||||
EOF
|
||||
print_success "Created docs/usage.md"
|
||||
|
||||
# Print summary
|
||||
echo ""
|
||||
print_success "Click CLI project created successfully!"
|
||||
echo ""
|
||||
print_info "Next steps:"
|
||||
echo " 1. cd $PROJECT_NAME"
|
||||
echo " 2. python -m venv venv"
|
||||
echo " 3. source venv/bin/activate"
|
||||
echo " 4. pip install -e ."
|
||||
echo " 5. $PROJECT_NAME --help"
|
||||
echo ""
|
||||
print_info "Project type: $CLI_TYPE"
|
||||
print_info "Location: $(pwd)/$PROJECT_NAME"
|
||||
108
skills/click-patterns/scripts/setup-click-project.sh
Executable file
108
skills/click-patterns/scripts/setup-click-project.sh
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# setup-click-project.sh - Setup Click project dependencies and environment
|
||||
#
|
||||
# Usage: setup-click-project.sh [project-directory]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_info() { echo -e "${CYAN}ℹ${NC} $1"; }
|
||||
print_success() { echo -e "${GREEN}✓${NC} $1"; }
|
||||
print_error() { echo -e "${RED}✗${NC} $1" >&2; }
|
||||
print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||
|
||||
PROJECT_DIR="${1:-.}"
|
||||
|
||||
print_info "Setting up Click project in: $PROJECT_DIR"
|
||||
|
||||
# Check if Python is installed
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
print_error "Python 3 is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PYTHON_VERSION=$(python3 --version | cut -d' ' -f2)
|
||||
print_success "Python $PYTHON_VERSION detected"
|
||||
|
||||
# Navigate to project directory
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# Check if virtual environment exists
|
||||
if [ ! -d "venv" ]; then
|
||||
print_info "Creating virtual environment..."
|
||||
python3 -m venv venv
|
||||
print_success "Virtual environment created"
|
||||
else
|
||||
print_info "Virtual environment already exists"
|
||||
fi
|
||||
|
||||
# Activate virtual environment
|
||||
print_info "Activating virtual environment..."
|
||||
source venv/bin/activate
|
||||
|
||||
# Upgrade pip
|
||||
print_info "Upgrading pip..."
|
||||
pip install --upgrade pip > /dev/null 2>&1
|
||||
print_success "pip upgraded"
|
||||
|
||||
# Install Click and dependencies
|
||||
print_info "Installing Click and dependencies..."
|
||||
if [ -f "requirements.txt" ]; then
|
||||
pip install -r requirements.txt
|
||||
print_success "Installed from requirements.txt"
|
||||
else
|
||||
pip install click rich
|
||||
print_success "Installed click and rich"
|
||||
fi
|
||||
|
||||
# Install development dependencies
|
||||
print_info "Installing development dependencies..."
|
||||
pip install pytest pytest-cov black pylint mypy
|
||||
print_success "Development dependencies installed"
|
||||
|
||||
# Create .env.example if it doesn't exist
|
||||
if [ ! -f ".env.example" ]; then
|
||||
cat > .env.example <<'EOF'
|
||||
# Environment variables for CLI
|
||||
API_KEY=your_api_key_here
|
||||
DEBUG=false
|
||||
LOG_LEVEL=info
|
||||
EOF
|
||||
print_success "Created .env.example"
|
||||
fi
|
||||
|
||||
# Setup pre-commit hooks if git repo
|
||||
if [ -d ".git" ]; then
|
||||
print_info "Setting up git hooks..."
|
||||
cat > .git/hooks/pre-commit <<'EOF'
|
||||
#!/bin/bash
|
||||
# Run tests before commit
|
||||
source venv/bin/activate
|
||||
black src/ tests/ --check || exit 1
|
||||
pylint src/ || exit 1
|
||||
pytest tests/ || exit 1
|
||||
EOF
|
||||
chmod +x .git/hooks/pre-commit
|
||||
print_success "Git hooks configured"
|
||||
fi
|
||||
|
||||
# Verify installation
|
||||
print_info "Verifying installation..."
|
||||
python3 -c "import click; print(f'Click version: {click.__version__}')"
|
||||
print_success "Click is properly installed"
|
||||
|
||||
echo ""
|
||||
print_success "Setup completed successfully!"
|
||||
echo ""
|
||||
print_info "Next steps:"
|
||||
echo " 1. source venv/bin/activate"
|
||||
echo " 2. python src/cli.py --help"
|
||||
echo " 3. pytest tests/"
|
||||
162
skills/click-patterns/scripts/validate-click.sh
Executable file
162
skills/click-patterns/scripts/validate-click.sh
Executable file
@@ -0,0 +1,162 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# validate-click.sh - Validate Click CLI implementation
|
||||
#
|
||||
# Usage: validate-click.sh <cli-file.py>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_info() { echo -e "${CYAN}ℹ${NC} $1"; }
|
||||
print_success() { echo -e "${GREEN}✓${NC} $1"; }
|
||||
print_error() { echo -e "${RED}✗${NC} $1" >&2; }
|
||||
print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||
|
||||
# Validate arguments
|
||||
if [ $# -lt 1 ]; then
|
||||
print_error "Usage: $0 <cli-file.py>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CLI_FILE="$1"
|
||||
|
||||
# Check if file exists
|
||||
if [ ! -f "$CLI_FILE" ]; then
|
||||
print_error "File not found: $CLI_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Validating Click CLI: $CLI_FILE"
|
||||
echo ""
|
||||
|
||||
VALIDATION_PASSED=true
|
||||
|
||||
# Check 1: File is a Python file
|
||||
if [[ ! "$CLI_FILE" =~ \.py$ ]]; then
|
||||
print_error "File must be a Python file (.py)"
|
||||
VALIDATION_PASSED=false
|
||||
else
|
||||
print_success "File extension is valid (.py)"
|
||||
fi
|
||||
|
||||
# Check 2: File imports Click
|
||||
if grep -q "import click" "$CLI_FILE"; then
|
||||
print_success "Click module is imported"
|
||||
else
|
||||
print_error "Click module is not imported"
|
||||
VALIDATION_PASSED=false
|
||||
fi
|
||||
|
||||
# Check 3: Has at least one Click decorator
|
||||
DECORATOR_COUNT=$(grep -c "@click\." "$CLI_FILE" || true)
|
||||
if [ "$DECORATOR_COUNT" -gt 0 ]; then
|
||||
print_success "Found $DECORATOR_COUNT Click decorator(s)"
|
||||
else
|
||||
print_error "No Click decorators found"
|
||||
VALIDATION_PASSED=false
|
||||
fi
|
||||
|
||||
# Check 4: Has main entry point or group
|
||||
if grep -q "@click.command()\|@click.group()" "$CLI_FILE"; then
|
||||
print_success "Has Click command or group decorator"
|
||||
else
|
||||
print_error "Missing @click.command() or @click.group()"
|
||||
VALIDATION_PASSED=false
|
||||
fi
|
||||
|
||||
# Check 5: Has if __name__ == '__main__' block
|
||||
if grep -q "if __name__ == '__main__':" "$CLI_FILE"; then
|
||||
print_success "Has main execution block"
|
||||
else
|
||||
print_warning "Missing main execution block (if __name__ == '__main__':)"
|
||||
fi
|
||||
|
||||
# Check 6: Python syntax is valid
|
||||
if python3 -m py_compile "$CLI_FILE" 2>/dev/null; then
|
||||
print_success "Python syntax is valid"
|
||||
else
|
||||
print_error "Python syntax errors detected"
|
||||
VALIDATION_PASSED=false
|
||||
fi
|
||||
|
||||
# Check 7: Has help text
|
||||
if grep -q '"""' "$CLI_FILE"; then
|
||||
print_success "Contains docstrings/help text"
|
||||
else
|
||||
print_warning "No docstrings found (recommended for help text)"
|
||||
fi
|
||||
|
||||
# Check 8: Has option or argument decorators
|
||||
if grep -q "@click.option\|@click.argument" "$CLI_FILE"; then
|
||||
print_success "Has options or arguments defined"
|
||||
else
|
||||
print_warning "No options or arguments defined"
|
||||
fi
|
||||
|
||||
# Check 9: Uses recommended patterns
|
||||
echo ""
|
||||
print_info "Checking best practices..."
|
||||
|
||||
# Check for version option
|
||||
if grep -q "@click.version_option" "$CLI_FILE"; then
|
||||
print_success "Has version option"
|
||||
else
|
||||
print_warning "Consider adding @click.version_option()"
|
||||
fi
|
||||
|
||||
# Check for help parameter
|
||||
if grep -q "help=" "$CLI_FILE"; then
|
||||
print_success "Uses help parameters"
|
||||
else
|
||||
print_warning "Consider adding help text to options"
|
||||
fi
|
||||
|
||||
# Check for context usage
|
||||
if grep -q "@click.pass_context" "$CLI_FILE"; then
|
||||
print_success "Uses context for state sharing"
|
||||
else
|
||||
print_info "No context usage detected (optional)"
|
||||
fi
|
||||
|
||||
# Check for command groups
|
||||
if grep -q "@click.group()" "$CLI_FILE"; then
|
||||
print_success "Uses command groups"
|
||||
# Check for subcommands
|
||||
SUBCOMMAND_COUNT=$(grep -c "\.command()" "$CLI_FILE" || true)
|
||||
if [ "$SUBCOMMAND_COUNT" -gt 0 ]; then
|
||||
print_success "Has $SUBCOMMAND_COUNT subcommand(s)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for validation
|
||||
if grep -q "click.Choice\|click.IntRange\|click.FloatRange\|click.Path" "$CLI_FILE"; then
|
||||
print_success "Uses Click's built-in validators"
|
||||
else
|
||||
print_info "No built-in validators detected (optional)"
|
||||
fi
|
||||
|
||||
# Check for colored output (Rich or Click's styling)
|
||||
if grep -q "from rich\|click.style\|click.echo.*fg=" "$CLI_FILE"; then
|
||||
print_success "Uses colored output"
|
||||
else
|
||||
print_info "No colored output detected (optional)"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
if [ "$VALIDATION_PASSED" = true ]; then
|
||||
print_success "All critical validations passed!"
|
||||
echo ""
|
||||
print_info "Try running: python3 $CLI_FILE --help"
|
||||
exit 0
|
||||
else
|
||||
print_error "Validation failed. Please fix the errors above."
|
||||
exit 1
|
||||
fi
|
||||
310
skills/click-patterns/templates/advanced-cli.py
Normal file
310
skills/click-patterns/templates/advanced-cli.py
Normal file
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Advanced Click CLI Template
|
||||
|
||||
Demonstrates advanced patterns including:
|
||||
- Custom parameter types
|
||||
- Command chaining
|
||||
- Plugin architecture
|
||||
- Configuration management
|
||||
- Logging integration
|
||||
"""
|
||||
|
||||
import click
|
||||
import logging
|
||||
from rich.console import Console
|
||||
from pathlib import Path
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
console = Console()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Custom parameter types
|
||||
class JsonType(click.ParamType):
|
||||
"""Custom type for JSON parsing"""
|
||||
name = 'json'
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except json.JSONDecodeError as e:
|
||||
self.fail(f'Invalid JSON: {e}', param, ctx)
|
||||
|
||||
|
||||
class PathListType(click.ParamType):
|
||||
"""Custom type for comma-separated paths"""
|
||||
name = 'pathlist'
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
paths = [Path(p.strip()) for p in value.split(',')]
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
self.fail(f'Path does not exist: {path}', param, ctx)
|
||||
return paths
|
||||
|
||||
|
||||
# Configuration class
|
||||
class Config:
|
||||
"""Application configuration"""
|
||||
|
||||
def __init__(self):
|
||||
self.debug = False
|
||||
self.log_level = 'INFO'
|
||||
self.config_file = 'config.json'
|
||||
self._data = {}
|
||||
|
||||
def load(self, config_file: Optional[str] = None):
|
||||
"""Load configuration from file"""
|
||||
file_path = Path(config_file or self.config_file)
|
||||
if file_path.exists():
|
||||
with open(file_path) as f:
|
||||
self._data = json.load(f)
|
||||
logger.info(f"Loaded config from {file_path}")
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
"""Get configuration value"""
|
||||
return self._data.get(key, default)
|
||||
|
||||
def set(self, key: str, value):
|
||||
"""Set configuration value"""
|
||||
self._data[key] = value
|
||||
|
||||
def save(self):
|
||||
"""Save configuration to file"""
|
||||
file_path = Path(self.config_file)
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(self._data, f, indent=2)
|
||||
logger.info(f"Saved config to {file_path}")
|
||||
|
||||
|
||||
# Pass config between commands
|
||||
pass_config = click.make_pass_decorator(Config, ensure=True)
|
||||
|
||||
|
||||
# Main CLI group
|
||||
@click.group(chain=True)
|
||||
@click.option('--debug', is_flag=True, help='Enable debug mode')
|
||||
@click.option('--config', type=click.Path(), default='config.json',
|
||||
help='Configuration file')
|
||||
@click.option('--log-level',
|
||||
type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR']),
|
||||
default='INFO',
|
||||
help='Logging level')
|
||||
@click.version_option(version='2.0.0')
|
||||
@pass_config
|
||||
def cli(config: Config, debug: bool, config: str, log_level: str):
|
||||
"""
|
||||
Advanced CLI with chaining and plugin support.
|
||||
|
||||
Commands can be chained together:
|
||||
cli init process deploy
|
||||
cli config set key=value process --validate
|
||||
"""
|
||||
config.debug = debug
|
||||
config.log_level = log_level
|
||||
config.config_file = config
|
||||
config.load()
|
||||
|
||||
# Set logging level
|
||||
logger.setLevel(getattr(logging, log_level))
|
||||
|
||||
if debug:
|
||||
console.print("[dim]Debug mode enabled[/dim]")
|
||||
|
||||
|
||||
# Pipeline commands (chainable)
|
||||
@cli.command()
|
||||
@click.option('--template', type=click.Choice(['basic', 'advanced', 'api']),
|
||||
default='basic')
|
||||
@pass_config
|
||||
def init(config: Config, template: str):
|
||||
"""Initialize project (chainable)"""
|
||||
console.print(f"[cyan]Initializing with {template} template...[/cyan]")
|
||||
config.set('template', template)
|
||||
return config
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--validate', is_flag=True, help='Validate before processing')
|
||||
@click.option('--parallel', is_flag=True, help='Process in parallel')
|
||||
@pass_config
|
||||
def process(config: Config, validate: bool, parallel: bool):
|
||||
"""Process data (chainable)"""
|
||||
console.print("[cyan]Processing data...[/cyan]")
|
||||
|
||||
if validate:
|
||||
console.print("[dim]Validating input...[/dim]")
|
||||
|
||||
mode = "parallel" if parallel else "sequential"
|
||||
console.print(f"[dim]Processing mode: {mode}[/dim]")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('environment', type=click.Choice(['dev', 'staging', 'prod']))
|
||||
@click.option('--dry-run', is_flag=True, help='Simulate deployment')
|
||||
@pass_config
|
||||
def deploy(config: Config, environment: str, dry_run: bool):
|
||||
"""Deploy to environment (chainable)"""
|
||||
prefix = "[yellow][DRY RUN][/yellow] " if dry_run else ""
|
||||
console.print(f"{prefix}[cyan]Deploying to {environment}...[/cyan]")
|
||||
|
||||
template = config.get('template', 'unknown')
|
||||
console.print(f"[dim]Template: {template}[/dim]")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
# Advanced configuration commands
|
||||
@cli.group()
|
||||
def config():
|
||||
"""Advanced configuration management"""
|
||||
pass
|
||||
|
||||
|
||||
@config.command()
|
||||
@click.argument('key')
|
||||
@pass_config
|
||||
def get(config: Config, key: str):
|
||||
"""Get configuration value"""
|
||||
value = config.get(key)
|
||||
if value is not None:
|
||||
console.print(f"{key}: [green]{value}[/green]")
|
||||
else:
|
||||
console.print(f"[yellow]Key not found: {key}[/yellow]")
|
||||
|
||||
|
||||
@config.command()
|
||||
@click.argument('pair')
|
||||
@pass_config
|
||||
def set(config: Config, pair: str):
|
||||
"""Set configuration (format: key=value)"""
|
||||
if '=' not in pair:
|
||||
raise click.BadParameter('Format must be key=value')
|
||||
|
||||
key, value = pair.split('=', 1)
|
||||
config.set(key, value)
|
||||
config.save()
|
||||
console.print(f"[green]✓[/green] Set {key} = {value}")
|
||||
|
||||
|
||||
@config.command()
|
||||
@click.option('--format', type=click.Choice(['json', 'yaml', 'env']),
|
||||
default='json')
|
||||
@pass_config
|
||||
def export(config: Config, format: str):
|
||||
"""Export configuration in different formats"""
|
||||
console.print(f"[cyan]Exporting config as {format}...[/cyan]")
|
||||
|
||||
if format == 'json':
|
||||
output = json.dumps(config._data, indent=2)
|
||||
elif format == 'yaml':
|
||||
# Simplified YAML output
|
||||
output = '\n'.join(f"{k}: {v}" for k, v in config._data.items())
|
||||
else: # env
|
||||
output = '\n'.join(f"{k.upper()}={v}" for k, v in config._data.items())
|
||||
|
||||
console.print(output)
|
||||
|
||||
|
||||
# Advanced data operations
|
||||
@cli.group()
|
||||
def data():
|
||||
"""Data operations with advanced types"""
|
||||
pass
|
||||
|
||||
|
||||
@data.command()
|
||||
@click.option('--json-data', type=JsonType(), help='JSON data to import')
|
||||
@click.option('--paths', type=PathListType(), help='Comma-separated paths')
|
||||
@pass_config
|
||||
def import_data(config: Config, json_data: Optional[dict], paths: Optional[list]):
|
||||
"""Import data from various sources"""
|
||||
console.print("[cyan]Importing data...[/cyan]")
|
||||
|
||||
if json_data:
|
||||
console.print(f"[dim]JSON data: {json_data}[/dim]")
|
||||
|
||||
if paths:
|
||||
console.print(f"[dim]Processing {len(paths)} path(s)[/dim]")
|
||||
for path in paths:
|
||||
console.print(f" - {path}")
|
||||
|
||||
|
||||
@data.command()
|
||||
@click.option('--input', type=click.File('r'), help='Input file')
|
||||
@click.option('--output', type=click.File('w'), help='Output file')
|
||||
@click.option('--format',
|
||||
type=click.Choice(['json', 'csv', 'xml']),
|
||||
default='json')
|
||||
def transform(input, output, format):
|
||||
"""Transform data between formats"""
|
||||
console.print(f"[cyan]Transforming data to {format}...[/cyan]")
|
||||
|
||||
if input:
|
||||
data = input.read()
|
||||
console.print(f"[dim]Read {len(data)} bytes[/dim]")
|
||||
|
||||
if output:
|
||||
# Would write transformed data here
|
||||
output.write('{}') # Placeholder
|
||||
console.print("[green]✓[/green] Transformation complete")
|
||||
|
||||
|
||||
# Plugin system
|
||||
@cli.group()
|
||||
def plugin():
|
||||
"""Plugin management"""
|
||||
pass
|
||||
|
||||
|
||||
@plugin.command()
|
||||
@click.argument('plugin_name')
|
||||
@click.option('--version', help='Plugin version')
|
||||
def install(plugin_name: str, version: Optional[str]):
|
||||
"""Install a plugin"""
|
||||
version_str = f"@{version}" if version else "@latest"
|
||||
console.print(f"[cyan]Installing plugin: {plugin_name}{version_str}...[/cyan]")
|
||||
console.print("[green]✓[/green] Plugin installed successfully")
|
||||
|
||||
|
||||
@plugin.command()
|
||||
def list():
|
||||
"""List installed plugins"""
|
||||
console.print("[cyan]Installed Plugins:[/cyan]")
|
||||
# Placeholder plugin list
|
||||
plugins = [
|
||||
{"name": "auth-plugin", "version": "1.0.0", "status": "active"},
|
||||
{"name": "database-plugin", "version": "2.1.0", "status": "active"},
|
||||
]
|
||||
for p in plugins:
|
||||
status_color = "green" if p["status"] == "active" else "yellow"
|
||||
console.print(f" - {p['name']} ({p['version']}) [{status_color}]{p['status']}[/{status_color}]")
|
||||
|
||||
|
||||
# Batch operations
|
||||
@cli.command()
|
||||
@click.argument('commands', nargs=-1, required=True)
|
||||
@pass_config
|
||||
def batch(config: Config, commands: tuple):
|
||||
"""Execute multiple commands in batch"""
|
||||
console.print(f"[cyan]Executing {len(commands)} command(s)...[/cyan]")
|
||||
|
||||
for i, cmd in enumerate(commands, 1):
|
||||
console.print(f"[dim]{i}. {cmd}[/dim]")
|
||||
# Would execute actual commands here
|
||||
|
||||
console.print("[green]✓[/green] Batch execution completed")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
37
skills/click-patterns/templates/basic-cli.py
Normal file
37
skills/click-patterns/templates/basic-cli.py
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Basic Click CLI Template
|
||||
|
||||
A simple single-command CLI using Click framework.
|
||||
"""
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.version_option(version='1.0.0')
|
||||
@click.option('--name', '-n', default='World', help='Name to greet')
|
||||
@click.option('--count', '-c', default=1, type=int, help='Number of greetings')
|
||||
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
|
||||
def cli(name, count, verbose):
|
||||
"""
|
||||
A simple greeting CLI tool.
|
||||
|
||||
Example:
|
||||
python cli.py --name Alice --count 3
|
||||
"""
|
||||
if verbose:
|
||||
console.print(f"[dim]Running with name={name}, count={count}[/dim]")
|
||||
|
||||
for i in range(count):
|
||||
console.print(f"[green]Hello, {name}![/green]")
|
||||
|
||||
if verbose:
|
||||
console.print(f"[dim]Completed {count} greeting(s)[/dim]")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
126
skills/click-patterns/templates/nested-commands.py
Normal file
126
skills/click-patterns/templates/nested-commands.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Nested Commands Click Template
|
||||
|
||||
Demonstrates command groups, nested subcommands, and context sharing.
|
||||
"""
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version='1.0.0')
|
||||
@click.pass_context
|
||||
def cli(ctx):
|
||||
"""
|
||||
A powerful CLI tool with nested commands.
|
||||
|
||||
Example:
|
||||
python cli.py init --template basic
|
||||
python cli.py deploy production --mode safe
|
||||
python cli.py config get api-key
|
||||
"""
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj['console'] = console
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--template', '-t', default='basic',
|
||||
type=click.Choice(['basic', 'advanced', 'minimal']),
|
||||
help='Project template')
|
||||
@click.pass_context
|
||||
def init(ctx, template):
|
||||
"""Initialize a new project"""
|
||||
console = ctx.obj['console']
|
||||
console.print(f"[green]✓[/green] Initializing project with {template} template...")
|
||||
|
||||
|
||||
@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.pass_context
|
||||
def deploy(ctx, environment, force, mode):
|
||||
"""Deploy to specified environment"""
|
||||
console = ctx.obj['console']
|
||||
console.print(f"[cyan]Deploying to {environment} in {mode} mode[/cyan]")
|
||||
if force:
|
||||
console.print("[yellow]⚠ Force mode enabled[/yellow]")
|
||||
|
||||
|
||||
@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']
|
||||
# Placeholder for actual config retrieval
|
||||
value = "example_value"
|
||||
console.print(f"[dim]Config[/dim] {key}: [green]{value}[/green]")
|
||||
|
||||
|
||||
@config.command()
|
||||
@click.argument('key')
|
||||
@click.argument('value')
|
||||
@click.pass_context
|
||||
def set(ctx, key, value):
|
||||
"""Set configuration value"""
|
||||
console = ctx.obj['console']
|
||||
# Placeholder for actual config storage
|
||||
console.print(f"[green]✓[/green] Set {key} = {value}")
|
||||
|
||||
|
||||
@config.command()
|
||||
@click.pass_context
|
||||
def list(ctx):
|
||||
"""List all configuration settings"""
|
||||
console = ctx.obj['console']
|
||||
console.print("[cyan]Configuration Settings:[/cyan]")
|
||||
# Placeholder for actual config listing
|
||||
console.print(" api-key: [dim]***hidden***[/dim]")
|
||||
console.print(" debug: [green]true[/green]")
|
||||
|
||||
|
||||
@cli.group()
|
||||
def database():
|
||||
"""Database management commands"""
|
||||
pass
|
||||
|
||||
|
||||
@database.command()
|
||||
@click.option('--create-tables', is_flag=True, help='Create tables')
|
||||
@click.pass_context
|
||||
def migrate(ctx, create_tables):
|
||||
"""Run database migrations"""
|
||||
console = ctx.obj['console']
|
||||
console.print("[cyan]Running migrations...[/cyan]")
|
||||
if create_tables:
|
||||
console.print("[green]✓[/green] Tables created")
|
||||
|
||||
|
||||
@database.command()
|
||||
@click.option('--confirm', is_flag=True, help='Confirm reset')
|
||||
@click.pass_context
|
||||
def reset(ctx, confirm):
|
||||
"""Reset database (destructive)"""
|
||||
console = ctx.obj['console']
|
||||
if not confirm:
|
||||
console.print("[yellow]⚠ Use --confirm to proceed[/yellow]")
|
||||
return
|
||||
console.print("[red]Resetting database...[/red]")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli(obj={})
|
||||
169
skills/click-patterns/templates/validators.py
Normal file
169
skills/click-patterns/templates/validators.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Click Custom Validators Template
|
||||
|
||||
Demonstrates custom parameter validation, callbacks, and type conversion.
|
||||
"""
|
||||
|
||||
import click
|
||||
import re
|
||||
from pathlib import Path
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
# Custom validator callbacks
|
||||
def validate_email(ctx, param, value):
|
||||
"""Validate email format"""
|
||||
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
|
||||
|
||||
|
||||
def validate_port(ctx, param, value):
|
||||
"""Validate port number"""
|
||||
if value < 1 or value > 65535:
|
||||
raise click.BadParameter('Port must be between 1 and 65535')
|
||||
return value
|
||||
|
||||
|
||||
def validate_path_exists(ctx, param, value):
|
||||
"""Validate that path exists"""
|
||||
if value and not Path(value).exists():
|
||||
raise click.BadParameter(f'Path does not exist: {value}')
|
||||
return value
|
||||
|
||||
|
||||
def validate_url(ctx, param, value):
|
||||
"""Validate URL format"""
|
||||
if value and not re.match(r'^https?://[^\s]+$', value):
|
||||
raise click.BadParameter('Invalid URL format (must start with http:// or https://)')
|
||||
return value
|
||||
|
||||
|
||||
# Custom Click types
|
||||
class CommaSeparatedList(click.ParamType):
|
||||
"""Custom type for comma-separated lists"""
|
||||
name = 'comma-list'
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
try:
|
||||
return [item.strip() for item in value.split(',') if item.strip()]
|
||||
except Exception:
|
||||
self.fail(f'{value} is not a valid comma-separated list', param, ctx)
|
||||
|
||||
|
||||
class EnvironmentVariable(click.ParamType):
|
||||
"""Custom type for environment variables"""
|
||||
name = 'env-var'
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if not re.match(r'^[A-Z_][A-Z0-9_]*$', value):
|
||||
self.fail(f'{value} is not a valid environment variable name', param, ctx)
|
||||
return value
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
"""CLI with custom validators"""
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--email', callback=validate_email, required=True, help='User email address')
|
||||
@click.option('--age', type=click.IntRange(0, 150), required=True, help='User age')
|
||||
@click.option('--username', type=click.STRING, required=True,
|
||||
help='Username (3-20 characters)',
|
||||
callback=lambda ctx, param, value: value if 3 <= len(value) <= 20
|
||||
else ctx.fail('Username must be 3-20 characters'))
|
||||
def create_user(email, age, username):
|
||||
"""Create a new user with validation"""
|
||||
console.print(f"[green]✓[/green] User created: {username} ({email}), age {age}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--port', type=int, callback=validate_port, default=8080, help='Server port')
|
||||
@click.option('--host', default='localhost', help='Server host')
|
||||
@click.option('--workers', type=click.IntRange(1, 32), default=4, help='Number of workers')
|
||||
@click.option('--ssl', is_flag=True, help='Enable SSL')
|
||||
def start_server(port, host, workers, ssl):
|
||||
"""Start server with validated parameters"""
|
||||
protocol = 'https' if ssl else 'http'
|
||||
console.print(f"[cyan]Starting server at {protocol}://{host}:{port}[/cyan]")
|
||||
console.print(f"[dim]Workers: {workers}[/dim]")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--config', type=click.Path(exists=True, dir_okay=False),
|
||||
callback=validate_path_exists, required=True, help='Config file path')
|
||||
@click.option('--output', type=click.Path(dir_okay=False), required=True, help='Output file path')
|
||||
@click.option('--format', type=click.Choice(['json', 'yaml', 'toml']), default='json',
|
||||
help='Output format')
|
||||
def convert_config(config, output, format):
|
||||
"""Convert configuration file"""
|
||||
console.print(f"[cyan]Converting {config} to {format} format[/cyan]")
|
||||
console.print(f"[green]✓[/green] Output: {output}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--url', callback=validate_url, required=True, help='API URL')
|
||||
@click.option('--method', type=click.Choice(['GET', 'POST', 'PUT', 'DELETE']),
|
||||
default='GET', help='HTTP method')
|
||||
@click.option('--headers', type=CommaSeparatedList(), help='Headers (comma-separated key:value)')
|
||||
@click.option('--timeout', type=click.FloatRange(0.1, 300.0), default=30.0,
|
||||
help='Request timeout in seconds')
|
||||
def api_call(url, method, headers, timeout):
|
||||
"""Make API call with validation"""
|
||||
console.print(f"[cyan]{method} {url}[/cyan]")
|
||||
console.print(f"[dim]Timeout: {timeout}s[/dim]")
|
||||
if headers:
|
||||
console.print(f"[dim]Headers: {headers}[/dim]")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--env-var', type=EnvironmentVariable(), required=True,
|
||||
help='Environment variable name')
|
||||
@click.option('--value', required=True, help='Environment variable value')
|
||||
@click.option('--scope', type=click.Choice(['user', 'system', 'project']),
|
||||
default='user', help='Variable scope')
|
||||
def set_env(env_var, value, scope):
|
||||
"""Set environment variable with validation"""
|
||||
console.print(f"[green]✓[/green] Set {env_var}={value} (scope: {scope})")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--min', type=float, required=True, help='Minimum value')
|
||||
@click.option('--max', type=float, required=True, help='Maximum value')
|
||||
@click.option('--step', type=click.FloatRange(0.01, None), default=1.0, help='Step size')
|
||||
def generate_range(min, max, step):
|
||||
"""Generate numeric range with validation"""
|
||||
if min >= max:
|
||||
raise click.BadParameter('min must be less than max')
|
||||
|
||||
count = int((max - min) / step) + 1
|
||||
console.print(f"[cyan]Generating range from {min} to {max} (step: {step})[/cyan]")
|
||||
console.print(f"[dim]Total values: {count}[/dim]")
|
||||
|
||||
|
||||
# Example combining multiple validators
|
||||
@cli.command()
|
||||
@click.option('--name', required=True, help='Project name',
|
||||
callback=lambda ctx, param, value: value.lower().replace(' ', '-'))
|
||||
@click.option('--tags', type=CommaSeparatedList(), help='Project tags (comma-separated)')
|
||||
@click.option('--priority', type=click.IntRange(1, 10), default=5, help='Priority (1-10)')
|
||||
@click.option('--template', type=click.Path(exists=True), help='Template directory')
|
||||
def create_project(name, tags, priority, template):
|
||||
"""Create project with multiple validators"""
|
||||
console.print(f"[green]✓[/green] Project created: {name}")
|
||||
console.print(f"[dim]Priority: {priority}[/dim]")
|
||||
if tags:
|
||||
console.print(f"[dim]Tags: {', '.join(tags)}[/dim]")
|
||||
if template:
|
||||
console.print(f"[dim]Template: {template}[/dim]")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
Reference in New Issue
Block a user