Initial commit

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

View File

@@ -0,0 +1,145 @@
# Typer Patterns - Quick Start
Modern type-safe CLI patterns for building maintainable command-line applications with Typer.
## Structure
```
typer-patterns/
├── SKILL.md # Main skill documentation
├── templates/ # 5 production-ready templates
│ ├── basic-typed-command.py # Simple type-safe CLI
│ ├── enum-options.py # Enum-based choices
│ ├── sub-app-structure.py # Multi-command hierarchy
│ ├── typer-instance.py # Factory pattern
│ └── advanced-validation.py # Custom validators
├── scripts/ # 5 helper scripts
│ ├── validate-types.sh # Type hint validation
│ ├── generate-cli.sh # CLI generator
│ ├── test-cli.sh # CLI testing
│ ├── convert-argparse.sh # Migration guide
│ └── validate-skill.sh # Skill validation
└── examples/ # 4 complete examples
├── basic-cli/ # Simple CLI example
├── enum-cli/ # Enum usage example
├── subapp-cli/ # Sub-commands example
└── factory-cli/ # Testable factory pattern
```
## Quick Usage
### Generate a new CLI
```bash
cd skills/typer-patterns
./scripts/generate-cli.sh basic my_cli.py --app-name myapp
```
### Validate type hints
```bash
./scripts/validate-types.sh my_cli.py
```
### Test CLI functionality
```bash
./scripts/test-cli.sh my_cli.py
```
## Templates at a Glance
| Template | Use Case | Key Features |
|----------|----------|--------------|
| **basic-typed-command.py** | Simple CLIs | Type hints, Path validation, Options |
| **enum-options.py** | Constrained choices | Enums, autocomplete, match/case |
| **sub-app-structure.py** | Complex CLIs | Sub-apps, shared context, hierarchy |
| **typer-instance.py** | Testable CLIs | Factory pattern, DI, mocking |
| **advanced-validation.py** | Custom validation | Callbacks, validators, protocols |
## Example: Quick CLI in 5 Minutes
1. **Copy template**
```bash
cp templates/basic-typed-command.py my_cli.py
```
2. **Customize**
```python
# Edit my_cli.py - change function names, add logic
```
3. **Validate**
```bash
./scripts/validate-types.sh my_cli.py
```
4. **Test**
```bash
python my_cli.py --help
```
## Type Safety Checklist
- [ ] All parameters have type hints
- [ ] Return types specified on all functions
- [ ] Use `Path` for file/directory parameters
- [ ] Use `Enum` for constrained choices
- [ ] Use `Optional[T]` for optional parameters
- [ ] Add docstrings for help text
## Common Patterns
### Type Hints
```python
def process(
input: Path = typer.Argument(...),
output: Optional[Path] = typer.Option(None),
count: int = typer.Option(10),
verbose: bool = typer.Option(False)
) -> None:
```
### Enums
```python
class Format(str, Enum):
json = "json"
yaml = "yaml"
def export(format: Format = typer.Option(Format.json)) -> None:
```
### Sub-Apps
```python
app = typer.Typer()
db_app = typer.Typer()
app.add_typer(db_app, name="db")
@db_app.command("migrate")
def db_migrate() -> None:
```
### Factory Pattern
```python
def create_app(config: Config) -> typer.Typer:
app = typer.Typer()
# Define commands with config access
return app
```
## Next Steps
1. Review `SKILL.md` for comprehensive patterns
2. Study `examples/` for working code
3. Use `scripts/` to automate common tasks
4. Customize templates for your use case
## Validation Results
Skill validation: **PASSED**
- SKILL.md: Valid frontmatter and structure
- Templates: 5 templates (minimum 4 required)
- Scripts: 5 scripts (minimum 3 required)
- Examples: 4 complete examples with READMEs
- Security: No hardcoded secrets detected

View File

@@ -0,0 +1,201 @@
---
name: typer-patterns
description: Modern type-safe Typer CLI patterns with type hints, Enums, and sub-apps. Use when building CLI applications, creating Typer commands, implementing type-safe CLIs, or when user mentions Typer, CLI patterns, type hints, Enums, sub-apps, or command-line interfaces.
allowed-tools: Read, Write, Edit, Bash
---
# typer-patterns
Provides modern type-safe Typer CLI patterns including type hints, Enum usage, sub-app composition, and Typer() instance patterns for building maintainable command-line applications.
## Core Patterns
### 1. Type-Safe Commands with Type Hints
Use Python type hints for automatic validation and better IDE support:
```python
import typer
from typing import Optional
from pathlib import Path
app = typer.Typer()
@app.command()
def process(
input_file: Path = typer.Argument(..., help="Input file path"),
output: Optional[Path] = typer.Option(None, help="Output file path"),
verbose: bool = typer.Option(False, "--verbose", "-v"),
count: int = typer.Option(10, help="Number of items to process")
) -> None:
"""Process files with type-safe parameters."""
if verbose:
typer.echo(f"Processing {input_file}")
```
### 2. Enum-Based Options
Use Enums for constrained choices with autocomplete:
```python
from enum import Enum
class OutputFormat(str, Enum):
json = "json"
yaml = "yaml"
text = "text"
@app.command()
def export(
format: OutputFormat = typer.Option(OutputFormat.json, help="Output format")
) -> None:
"""Export with enum-based format selection."""
typer.echo(f"Exporting as {format.value}")
```
### 3. Sub-Application Composition
Organize complex CLIs with sub-apps:
```python
app = typer.Typer()
db_app = typer.Typer()
app.add_typer(db_app, name="db", help="Database commands")
@db_app.command("migrate")
def db_migrate() -> None:
"""Run database migrations."""
pass
@db_app.command("seed")
def db_seed() -> None:
"""Seed database with test data."""
pass
```
### 4. Typer() Instance Pattern
Use Typer() instances for better organization and testing:
```python
def create_app() -> typer.Typer:
"""Factory function for creating Typer app."""
app = typer.Typer(
name="myapp",
help="My CLI application",
add_completion=True,
no_args_is_help=True
)
@app.command()
def hello(name: str) -> None:
typer.echo(f"Hello {name}")
return app
app = create_app()
if __name__ == "__main__":
app()
```
## Usage Workflow
1. **Identify pattern need**: Determine which Typer pattern fits your use case
2. **Select template**: Choose from templates/ based on complexity
3. **Customize**: Adapt type hints, Enums, and sub-apps to your domain
4. **Validate**: Run validation script to check type safety
5. **Test**: Use example tests as reference
## Template Selection Guide
- **basic-typed-command.py**: Single command with type hints
- **enum-options.py**: Commands with Enum-based options
- **sub-app-structure.py**: Multi-command CLI with sub-apps
- **typer-instance.py**: Factory pattern for testable CLIs
- **advanced-validation.py**: Custom validators and callbacks
## Validation
Run the type safety validation:
```bash
./scripts/validate-types.sh path/to/cli.py
```
Checks:
- All parameters have type hints
- Return types specified
- Enums used for constrained choices
- Proper Typer decorators
## Examples
See `examples/` for complete working CLIs:
- `examples/basic-cli/`: Simple typed CLI
- `examples/enum-cli/`: Enum-based options
- `examples/subapp-cli/`: Multi-command with sub-apps
- `examples/factory-cli/`: Testable Typer factory pattern
## Best Practices
1. **Always use type hints**: Enables auto-validation and IDE support
2. **Prefer Enums over strings**: For constrained choices
3. **Use Path for file paths**: Better validation than str
4. **Document with docstrings**: Typer uses them for help text
5. **Keep commands focused**: One command = one responsibility
6. **Use sub-apps for grouping**: Organize related commands together
7. **Test with factory pattern**: Makes CLIs unit-testable
## Common Patterns
### Callback for Global Options
```python
@app.callback()
def main(
verbose: bool = typer.Option(False, "--verbose", "-v"),
ctx: typer.Context = typer.Context
) -> None:
"""Global options applied to all commands."""
ctx.obj = {"verbose": verbose}
```
### Custom Validators
```python
def validate_port(value: int) -> int:
if not 1024 <= value <= 65535:
raise typer.BadParameter("Port must be between 1024-65535")
return value
@app.command()
def serve(port: int = typer.Option(8000, callback=validate_port)) -> None:
"""Serve with validated port."""
pass
```
### Rich Output Integration
```python
from rich.console import Console
console = Console()
@app.command()
def status() -> None:
"""Show status with rich formatting."""
console.print("[bold green]System online[/bold green]")
```
## Integration Points
- Use with `cli-structure` skill for overall CLI architecture
- Combine with `testing-patterns` for CLI test coverage
- Integrate with `packaging` skill for distribution
## References
- Templates: `templates/`
- Scripts: `scripts/validate-types.sh`, `scripts/generate-cli.sh`
- Examples: `examples/*/`

View File

@@ -0,0 +1,50 @@
# Basic CLI Example
Simple type-safe CLI demonstrating fundamental Typer patterns.
## Features
- Type hints on all parameters
- Path validation
- Optional output file
- Boolean flags
- Verbose mode
## Usage
```bash
# Process and display
python cli.py input.txt
# Process and save
python cli.py input.txt --output result.txt
# Convert to uppercase
python cli.py input.txt --uppercase
# Verbose output
python cli.py input.txt --verbose
```
## Testing
```bash
# Create test file
echo "hello world" > test.txt
# Run CLI
python cli.py test.txt --uppercase
# Output: HELLO WORLD
# Save to file
python cli.py test.txt --output out.txt --uppercase --verbose
# Outputs: Processing: test.txt
# ✓ Written to: out.txt
```
## Key Patterns
1. **Path type**: Automatic validation of file existence
2. **Optional parameters**: Using `Optional[Path]` for optional output
3. **Boolean flags**: Simple `bool` type for flags
4. **Colored output**: Using `typer.secho()` for success messages

View File

@@ -0,0 +1,43 @@
"""Basic CLI example using type hints.
This example demonstrates:
- Simple type-safe command
- Path validation
- Optional parameters
- Verbose output
"""
import typer
from pathlib import Path
from typing import Optional
app = typer.Typer(help="Basic file processing CLI")
@app.command()
def process(
input_file: Path = typer.Argument(
..., help="Input file to process", exists=True
),
output: Optional[Path] = typer.Option(None, "--output", "-o"),
uppercase: bool = typer.Option(False, "--uppercase", "-u"),
verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
"""Process text file with optional transformations."""
if verbose:
typer.echo(f"Processing: {input_file}")
content = input_file.read_text()
if uppercase:
content = content.upper()
if output:
output.write_text(content)
typer.secho(f"✓ Written to: {output}", fg=typer.colors.GREEN)
else:
typer.echo(content)
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,66 @@
# Enum CLI Example
Type-safe CLI using Enums for constrained choices.
## Features
- Multiple Enum types (LogLevel, OutputFormat)
- Autocomplete support
- Type-safe matching with match/case
- Validated input values
## Usage
```bash
# Export as JSON (default)
python cli.py export
# Export as YAML
python cli.py export yaml
# Export with custom log level
python cli.py export json --log-level debug
# Save to file
python cli.py export yaml --output config.yaml
# Validate log level
python cli.py validate info
```
## Testing
```bash
# Export JSON to console
python cli.py export json
# Output: {"app": "example", "version": "1.0.0", ...}
# Export YAML to file
python cli.py export yaml --output test.yaml --log-level warning
# Creates test.yaml with YAML format
# Validate enum value
python cli.py validate error
# Output: Log Level: error
# Severity: High - error messages
# Invalid enum (will fail with helpful message)
python cli.py export invalid
# Error: Invalid value for 'FORMAT': 'invalid' is not one of 'json', 'yaml', 'text'.
```
## Key Patterns
1. **Enum as Argument**: `format: OutputFormat = typer.Argument(...)`
2. **Enum as Option**: `log_level: LogLevel = typer.Option(...)`
3. **String Enum**: Inherit from `str, Enum` for string values
4. **Match/Case**: Use pattern matching with enum values
5. **Autocomplete**: Automatic shell completion for enum values
## Benefits
- Type safety at runtime and compile time
- IDE autocomplete for enum values
- Automatic validation of inputs
- Self-documenting constrained choices
- Easy to extend with new values

View File

@@ -0,0 +1,102 @@
"""Enum-based CLI example.
This example demonstrates:
- Enum usage for constrained choices
- Multiple enum types
- Autocomplete with enums
- Match/case with enum values
"""
import typer
from enum import Enum
from typing import Optional
import json
class LogLevel(str, Enum):
"""Logging levels."""
debug = "debug"
info = "info"
warning = "warning"
error = "error"
class OutputFormat(str, Enum):
"""Output formats."""
json = "json"
yaml = "yaml"
text = "text"
app = typer.Typer(help="Configuration export CLI with Enums")
@app.command()
def export(
format: OutputFormat = typer.Argument(
OutputFormat.json, help="Export format"
),
log_level: LogLevel = typer.Option(
LogLevel.info, "--log-level", "-l", help="Logging level"
),
output: Optional[str] = typer.Option(None, "--output", "-o"),
) -> None:
"""Export configuration in specified format.
The format parameter uses an Enum, providing:
- Autocomplete in the shell
- Validation of input values
- Type safety in code
"""
# Sample data
data = {
"app": "example",
"version": "1.0.0",
"log_level": log_level.value,
"features": ["auth", "api", "cache"],
}
# Format output based on enum
match format:
case OutputFormat.json:
output_text = json.dumps(data, indent=2)
case OutputFormat.yaml:
# Simplified YAML output
output_text = "\n".join(f"{k}: {v}" for k, v in data.items())
case OutputFormat.text:
output_text = "\n".join(
f"{k.upper()}: {v}" for k, v in data.items()
)
# Output
if output:
with open(output, "w") as f:
f.write(output_text)
typer.secho(f"✓ Exported to {output}", fg=typer.colors.GREEN)
else:
typer.echo(output_text)
@app.command()
def validate(
level: LogLevel = typer.Argument(..., help="Log level to validate")
) -> None:
"""Validate and display log level information."""
typer.echo(f"Log Level: {level.value}")
# Access enum properties
match level:
case LogLevel.debug:
typer.echo("Severity: Lowest - detailed debugging information")
case LogLevel.info:
typer.echo("Severity: Low - informational messages")
case LogLevel.warning:
typer.echo("Severity: Medium - warning messages")
case LogLevel.error:
typer.echo("Severity: High - error messages")
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,128 @@
# Factory Pattern CLI Example
Testable CLI using factory pattern with dependency injection.
## Features
- Factory function for app creation
- Dependency injection via Protocol
- Multiple storage implementations
- Configuration injection
- Highly testable structure
## Usage
```bash
# Save data
python cli.py save name "John Doe"
python cli.py save email "john@example.com"
# Load data
python cli.py load name
# Output: John Doe
python cli.py load email
# Output: john@example.com
# Show configuration
python cli.py config-show
# Use verbose mode
python cli.py --verbose save status "active"
# Custom data directory
python cli.py --data-dir /tmp/mydata save test "value"
```
## Testing Example
```python
# test_cli.py
from cli import create_app, Config, MemoryStorage
from typer.testing import CliRunner
def test_save_and_load():
"""Test save and load commands."""
# Create test configuration
config = Config(verbose=True)
storage = MemoryStorage()
# Create app with test dependencies
app = create_app(config=config, storage=storage)
# Test runner
runner = CliRunner()
# Test save
result = runner.invoke(app, ["save", "test_key", "test_value"])
assert result.exit_code == 0
assert "Saved test_key" in result.output
# Test load
result = runner.invoke(app, ["load", "test_key"])
assert result.exit_code == 0
assert "test_value" in result.output
```
Run tests:
```bash
pytest test_cli.py
```
## Architecture
### Factory Function
```python
def create_app(config: Config, storage: Storage) -> typer.Typer:
"""Create app with injected dependencies."""
```
### Storage Protocol
```python
class Storage(Protocol):
def save(self, key: str, value: str) -> None: ...
def load(self, key: str) -> str: ...
```
### Implementations
- `MemoryStorage`: In-memory storage (for testing)
- `FileStorage`: File-based storage (for production)
## Key Patterns
1. **Factory Function**: Returns configured Typer app
2. **Protocol Types**: Interface for dependency injection
3. **Dataclass Config**: Type-safe configuration
4. **Dependency Injection**: Pass storage and config to factory
5. **Testability**: Easy to mock dependencies in tests
## Benefits
- Unit testable without file I/O
- Swap implementations easily
- Configuration flexibility
- Clean dependency management
- Follows SOLID principles
- Easy to extend with new storage types
## Extension Example
Add new storage type:
```python
class DatabaseStorage:
"""Database storage implementation."""
def __init__(self, connection_string: str) -> None:
self.conn = connect(connection_string)
def save(self, key: str, value: str) -> None:
self.conn.execute("INSERT INTO data VALUES (?, ?)", (key, value))
def load(self, key: str) -> str:
return self.conn.execute("SELECT value FROM data WHERE key = ?", (key,)).fetchone()
# Use it
storage = DatabaseStorage("postgresql://localhost/mydb")
app = create_app(storage=storage)
```

View File

@@ -0,0 +1,162 @@
"""Factory pattern CLI example.
This example demonstrates:
- Factory function for app creation
- Dependency injection
- Testable CLI structure
- Configuration injection
"""
import typer
from typing import Protocol, Optional
from dataclasses import dataclass
from pathlib import Path
@dataclass
class Config:
"""Application configuration."""
verbose: bool = False
data_dir: Path = Path("./data")
max_items: int = 100
class Storage(Protocol):
"""Storage interface for dependency injection."""
def save(self, key: str, value: str) -> None:
"""Save data."""
...
def load(self, key: str) -> str:
"""Load data."""
...
class MemoryStorage:
"""In-memory storage implementation."""
def __init__(self) -> None:
self.data: dict[str, str] = {}
def save(self, key: str, value: str) -> None:
"""Save to memory."""
self.data[key] = value
def load(self, key: str) -> str:
"""Load from memory."""
return self.data.get(key, "")
class FileStorage:
"""File-based storage implementation."""
def __init__(self, base_dir: Path) -> None:
self.base_dir = base_dir
self.base_dir.mkdir(exist_ok=True)
def save(self, key: str, value: str) -> None:
"""Save to file."""
file_path = self.base_dir / f"{key}.txt"
file_path.write_text(value)
def load(self, key: str) -> str:
"""Load from file."""
file_path = self.base_dir / f"{key}.txt"
return file_path.read_text() if file_path.exists() else ""
def create_app(config: Optional[Config] = None, storage: Optional[Storage] = None) -> typer.Typer:
"""Factory function to create Typer app with dependencies.
This pattern enables:
- Dependency injection for testing
- Configuration flexibility
- Multiple app instances
- Easier unit testing
Args:
config: Application configuration
storage: Storage implementation
Returns:
Configured Typer application
"""
config = config or Config()
storage = storage or FileStorage(config.data_dir)
app = typer.Typer(
help="Data management CLI with factory pattern",
no_args_is_help=True,
)
@app.command()
def save(
key: str = typer.Argument(..., help="Data key"),
value: str = typer.Argument(..., help="Data value"),
) -> None:
"""Save data using injected storage."""
if config.verbose:
typer.echo(f"Saving {key}={value}")
try:
storage.save(key, value)
typer.secho(f"✓ Saved {key}", fg=typer.colors.GREEN)
except Exception as e:
typer.secho(f"✗ Error: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
@app.command()
def load(key: str = typer.Argument(..., help="Data key")) -> None:
"""Load data using injected storage."""
if config.verbose:
typer.echo(f"Loading {key}")
try:
value = storage.load(key)
if value:
typer.echo(value)
else:
typer.secho(f"✗ Key not found: {key}", fg=typer.colors.YELLOW)
except Exception as e:
typer.secho(f"✗ Error: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
@app.command()
def config_show() -> None:
"""Show current configuration."""
typer.echo("Configuration:")
typer.echo(f" Verbose: {config.verbose}")
typer.echo(f" Data dir: {config.data_dir}")
typer.echo(f" Max items: {config.max_items}")
return app
def main() -> None:
"""Main entry point with configuration."""
import sys
# Parse global flags
verbose = "--verbose" in sys.argv or "-v" in sys.argv
data_dir = Path("./data")
# Check for custom data directory
if "--data-dir" in sys.argv:
idx = sys.argv.index("--data-dir")
if idx + 1 < len(sys.argv):
data_dir = Path(sys.argv[idx + 1])
sys.argv.pop(idx) # Remove flag
sys.argv.pop(idx) # Remove value
# Create configuration
config = Config(verbose=verbose, data_dir=data_dir)
# Create and run app
app = create_app(config=config)
app()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,77 @@
# Sub-Application CLI Example
Multi-level CLI with organized command groups.
## Features
- Three sub-apps: db, server, user
- Shared context via callback
- Global options (--config, --verbose)
- Clean command hierarchy
- Logical command grouping
## Usage
```bash
# Show main help
python cli.py --help
# Show sub-app help
python cli.py db --help
python cli.py server --help
python cli.py user --help
# Database commands
python cli.py db init
python cli.py db migrate --steps 5
python cli.py db seed
# Server commands
python cli.py server start --port 8080
python cli.py server stop
python cli.py server restart
# User commands
python cli.py user create alice --email alice@example.com
python cli.py user create bob --email bob@example.com --admin
python cli.py user list
python cli.py user delete alice
# Global options
python cli.py --verbose db migrate
python cli.py --config prod.yaml server start
```
## Command Structure
```
cli.py
├── db
│ ├── init - Initialize database
│ ├── migrate - Run migrations
│ └── seed - Seed test data
├── server
│ ├── start - Start server
│ ├── stop - Stop server
│ └── restart - Restart server
└── user
├── create - Create user
├── delete - Delete user
└── list - List users
```
## Key Patterns
1. **Sub-App Creation**: `db_app = typer.Typer()`
2. **Adding Sub-Apps**: `app.add_typer(db_app, name="db")`
3. **Global Callback**: `@app.callback()` for shared options
4. **Context Sharing**: `ctx.obj` for passing data to sub-commands
5. **Command Organization**: Group related commands in sub-apps
## Benefits
- Clear command hierarchy
- Easier navigation with help text
- Logical grouping of functionality
- Shared configuration across commands
- Scalable structure for large CLIs

View File

@@ -0,0 +1,132 @@
"""Sub-application CLI example.
This example demonstrates:
- Multi-level command structure
- Sub-apps for logical grouping
- Shared context across commands
- Clean command organization
"""
import typer
from typing import Optional
from pathlib import Path
# Main app
app = typer.Typer(
help="Project management CLI with sub-commands", add_completion=True
)
# Sub-applications
db_app = typer.Typer(help="Database commands")
server_app = typer.Typer(help="Server commands")
user_app = typer.Typer(help="User management commands")
# Add sub-apps to main app
app.add_typer(db_app, name="db")
app.add_typer(server_app, name="server")
app.add_typer(user_app, name="user")
# Global callback for shared options
@app.callback()
def main(
ctx: typer.Context,
config: Optional[Path] = typer.Option(None, "--config", "-c"),
verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
"""Global options for all commands."""
ctx.obj = {"config": config, "verbose": verbose}
if verbose:
typer.echo(f"Config: {config or 'default'}")
# Database commands
@db_app.command("init")
def db_init(ctx: typer.Context) -> None:
"""Initialize database."""
if ctx.obj["verbose"]:
typer.echo("Initializing database...")
typer.secho("✓ Database initialized", fg=typer.colors.GREEN)
@db_app.command("migrate")
def db_migrate(ctx: typer.Context, steps: int = typer.Option(1)) -> None:
"""Run database migrations."""
if ctx.obj["verbose"]:
typer.echo(f"Running {steps} migration(s)...")
typer.secho("✓ Migrations complete", fg=typer.colors.GREEN)
@db_app.command("seed")
def db_seed(ctx: typer.Context) -> None:
"""Seed database with test data."""
if ctx.obj["verbose"]:
typer.echo("Seeding database...")
typer.secho("✓ Database seeded", fg=typer.colors.GREEN)
# Server commands
@server_app.command("start")
def server_start(
ctx: typer.Context,
port: int = typer.Option(8000, "--port", "-p"),
host: str = typer.Option("127.0.0.1", "--host"),
) -> None:
"""Start application server."""
if ctx.obj["verbose"]:
typer.echo(f"Starting server on {host}:{port}...")
typer.secho(f"✓ Server running at http://{host}:{port}", fg=typer.colors.GREEN)
@server_app.command("stop")
def server_stop(ctx: typer.Context) -> None:
"""Stop application server."""
if ctx.obj["verbose"]:
typer.echo("Stopping server...")
typer.secho("✓ Server stopped", fg=typer.colors.RED)
@server_app.command("restart")
def server_restart(ctx: typer.Context) -> None:
"""Restart application server."""
if ctx.obj["verbose"]:
typer.echo("Restarting server...")
typer.secho("✓ Server restarted", fg=typer.colors.GREEN)
# User commands
@user_app.command("create")
def user_create(
ctx: typer.Context,
username: str = typer.Argument(...),
email: str = typer.Option(..., "--email", "-e"),
admin: bool = typer.Option(False, "--admin"),
) -> None:
"""Create a new user."""
if ctx.obj["verbose"]:
typer.echo(f"Creating user: {username}")
role = "admin" if admin else "user"
typer.secho(f"✓ User {username} created as {role}", fg=typer.colors.GREEN)
@user_app.command("delete")
def user_delete(ctx: typer.Context, username: str = typer.Argument(...)) -> None:
"""Delete a user."""
confirm = typer.confirm(f"Delete user {username}?")
if not confirm:
typer.echo("Cancelled")
raise typer.Abort()
typer.secho(f"✓ User {username} deleted", fg=typer.colors.RED)
@user_app.command("list")
def user_list(ctx: typer.Context) -> None:
"""List all users."""
users = ["alice", "bob", "charlie"]
typer.echo("Users:")
for user in users:
typer.echo(f" - {user}")
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,105 @@
#!/bin/bash
# Helper script to convert argparse CLI to Typer (guidance)
set -euo pipefail
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
cat << 'EOF'
Converting argparse to Typer
=============================
This script provides guidance on converting argparse CLIs to Typer.
Common Conversions:
-------------------
1. Argument Parser Setup
argparse: parser = ArgumentParser()
Typer: app = typer.Typer()
2. Positional Arguments
argparse: parser.add_argument('name')
Typer: name: str = typer.Argument(...)
3. Optional Arguments
argparse: parser.add_argument('--flag', '-f')
Typer: flag: bool = typer.Option(False, '--flag', '-f')
4. Required Options
argparse: parser.add_argument('--name', required=True)
Typer: name: str = typer.Option(...)
5. Default Values
argparse: parser.add_argument('--count', default=10)
Typer: count: int = typer.Option(10)
6. Type Conversion
argparse: parser.add_argument('--port', type=int)
Typer: port: int = typer.Option(8000)
7. Choices/Enums
argparse: parser.add_argument('--format', choices=['json', 'yaml'])
Typer: format: Format = typer.Option(Format.json) # Format is Enum
8. File Arguments
argparse: parser.add_argument('--input', type=argparse.FileType('r'))
Typer: input: Path = typer.Option(...)
9. Help Text
argparse: parser.add_argument('--name', help='User name')
Typer: name: str = typer.Option(..., help='User name')
10. Subcommands
argparse: subparsers = parser.add_subparsers()
Typer: sub_app = typer.Typer(); app.add_typer(sub_app, name='sub')
Example Conversion:
-------------------
BEFORE (argparse):
parser = ArgumentParser()
parser.add_argument('input', help='Input file')
parser.add_argument('--output', '-o', help='Output file')
parser.add_argument('--verbose', '-v', action='store_true')
args = parser.parse_args()
AFTER (Typer):
app = typer.Typer()
@app.command()
def main(
input: Path = typer.Argument(..., help='Input file'),
output: Optional[Path] = typer.Option(None, '--output', '-o'),
verbose: bool = typer.Option(False, '--verbose', '-v')
) -> None:
"""Process input file."""
pass
if __name__ == '__main__':
app()
Benefits of Typer:
------------------
✓ Automatic type validation
✓ Better IDE support with type hints
✓ Less boilerplate code
✓ Built-in help generation
✓ Easier testing
✓ Rich formatting support
Next Steps:
-----------
1. Identify all argparse patterns in your CLI
2. Use templates from this skill as reference
3. Convert incrementally, one command at a time
4. Run validation: ./scripts/validate-types.sh
5. Test thoroughly: ./scripts/test-cli.sh
EOF
echo -e "${BLUE}For specific conversion help, provide your argparse CLI code.${NC}"

View File

@@ -0,0 +1,113 @@
#!/bin/bash
# Generate a Typer CLI from template
set -euo pipefail
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Usage
usage() {
cat << EOF
Usage: $0 <template-name> <output-file> [options]
Templates:
basic - Basic typed command
enum - Enum-based options
subapp - Sub-application structure
factory - Factory pattern
validation - Advanced validation
Options:
--app-name NAME Set application name (default: mycli)
--help Show this help
Example:
$0 basic my_cli.py --app-name myapp
EOF
exit 0
}
# Parse arguments
if [ $# -lt 2 ]; then
usage
fi
TEMPLATE="$1"
OUTPUT="$2"
APP_NAME="mycli"
shift 2
while [ $# -gt 0 ]; do
case "$1" in
--app-name)
APP_NAME="$2"
shift 2
;;
--help)
usage
;;
*)
echo "Unknown option: $1"
usage
;;
esac
done
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMPLATE_DIR="$(dirname "$SCRIPT_DIR")/templates"
# Map template name to file
case "$TEMPLATE" in
basic)
TEMPLATE_FILE="$TEMPLATE_DIR/basic-typed-command.py"
;;
enum)
TEMPLATE_FILE="$TEMPLATE_DIR/enum-options.py"
;;
subapp)
TEMPLATE_FILE="$TEMPLATE_DIR/sub-app-structure.py"
;;
factory)
TEMPLATE_FILE="$TEMPLATE_DIR/typer-instance.py"
;;
validation)
TEMPLATE_FILE="$TEMPLATE_DIR/advanced-validation.py"
;;
*)
echo "Unknown template: $TEMPLATE"
usage
;;
esac
# Check if template exists
if [ ! -f "$TEMPLATE_FILE" ]; then
echo -e "${YELLOW}✗ Template not found: $TEMPLATE_FILE${NC}"
exit 1
fi
# Copy and customize template
cp "$TEMPLATE_FILE" "$OUTPUT"
# Replace app name if not default
if [ "$APP_NAME" != "mycli" ]; then
sed -i "s/mycli/$APP_NAME/g" "$OUTPUT"
sed -i "s/myapp/$APP_NAME/g" "$OUTPUT"
fi
# Make executable
chmod +x "$OUTPUT"
echo -e "${GREEN}✓ Generated CLI: $OUTPUT${NC}"
echo " Template: $TEMPLATE"
echo " App name: $APP_NAME"
echo ""
echo "Next steps:"
echo " 1. Review and customize the generated file"
echo " 2. Install dependencies: pip install typer"
echo " 3. Run: python $OUTPUT --help"
echo " 4. Validate: ./scripts/validate-types.sh $OUTPUT"

View File

@@ -0,0 +1,138 @@
#!/bin/bash
# Test Typer CLI functionality
set -euo pipefail
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Check if file provided
if [ $# -eq 0 ]; then
echo -e "${RED}✗ Usage: $0 <python-file>${NC}"
exit 1
fi
CLI_FILE="$1"
# Check if file exists
if [ ! -f "$CLI_FILE" ]; then
echo -e "${RED}✗ File not found: $CLI_FILE${NC}"
exit 1
fi
echo "Testing Typer CLI: $CLI_FILE"
echo "========================================"
TESTS_PASSED=0
TESTS_FAILED=0
# Test: Help command
echo "Test: Help output"
if python "$CLI_FILE" --help > /dev/null 2>&1; then
echo -e "${GREEN}✓ Help command works${NC}"
((TESTS_PASSED++))
else
echo -e "${RED}✗ Help command failed${NC}"
((TESTS_FAILED++))
fi
# Test: Version flag (if supported)
echo "Test: Version flag"
if python "$CLI_FILE" --version > /dev/null 2>&1; then
echo -e "${GREEN}✓ Version flag works${NC}"
((TESTS_PASSED++))
elif grep -q "version" "$CLI_FILE"; then
echo -e "${YELLOW}⚠ Version defined but flag not working${NC}"
else
echo -e "${YELLOW}⚠ No version flag (optional)${NC}"
fi
# Test: Check for syntax errors
echo "Test: Python syntax"
if python -m py_compile "$CLI_FILE" 2>/dev/null; then
echo -e "${GREEN}✓ No syntax errors${NC}"
((TESTS_PASSED++))
else
echo -e "${RED}✗ Syntax errors detected${NC}"
((TESTS_FAILED++))
fi
# Test: Type checking with mypy (if available)
echo "Test: Type checking"
if command -v mypy &> /dev/null; then
if mypy "$CLI_FILE" --ignore-missing-imports 2>/dev/null; then
echo -e "${GREEN}✓ Type checking passed${NC}"
((TESTS_PASSED++))
else
echo -e "${YELLOW}⚠ Type checking warnings/errors${NC}"
echo " Run: mypy $CLI_FILE --ignore-missing-imports"
fi
else
echo -e "${YELLOW}⚠ mypy not installed (skipping type check)${NC}"
fi
# Test: Linting with ruff (if available)
echo "Test: Code linting"
if command -v ruff &> /dev/null; then
if ruff check "$CLI_FILE" --select E,W,F 2>/dev/null; then
echo -e "${GREEN}✓ Linting passed${NC}"
((TESTS_PASSED++))
else
echo -e "${YELLOW}⚠ Linting warnings/errors${NC}"
echo " Run: ruff check $CLI_FILE"
fi
else
echo -e "${YELLOW}⚠ ruff not installed (skipping linting)${NC}"
fi
# Test: Import check
echo "Test: Import dependencies"
if python -c "import sys; sys.path.insert(0, '.'); exec(open('$CLI_FILE').read().split('if __name__')[0])" 2>/dev/null; then
echo -e "${GREEN}✓ All imports successful${NC}"
((TESTS_PASSED++))
else
echo -e "${RED}✗ Import errors detected${NC}"
echo " Check that all dependencies are installed"
((TESTS_FAILED++))
fi
# Test: Check for common patterns
echo "Test: Typer patterns"
PATTERN_ISSUES=0
if ! grep -q "@app.command()" "$CLI_FILE"; then
echo -e "${YELLOW} ⚠ No @app.command() decorators found${NC}"
((PATTERN_ISSUES++))
fi
if ! grep -q "typer.Typer()" "$CLI_FILE"; then
echo -e "${YELLOW} ⚠ No Typer() instance found${NC}"
((PATTERN_ISSUES++))
fi
if ! grep -q "if __name__ == \"__main__\":" "$CLI_FILE"; then
echo -e "${YELLOW} ⚠ Missing if __name__ == '__main__' guard${NC}"
((PATTERN_ISSUES++))
fi
if [ $PATTERN_ISSUES -eq 0 ]; then
echo -e "${GREEN}✓ Common patterns found${NC}"
((TESTS_PASSED++))
else
echo -e "${YELLOW}⚠ Some patterns missing${NC}"
fi
echo "========================================"
echo "Tests passed: $TESTS_PASSED"
echo "Tests failed: $TESTS_FAILED"
if [ $TESTS_FAILED -eq 0 ]; then
echo -e "${GREEN}✓ All tests passed!${NC}"
exit 0
else
echo -e "${RED}✗ Some tests failed${NC}"
exit 1
fi

View File

@@ -0,0 +1,135 @@
#!/bin/bash
# Validate typer-patterns skill structure
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ERRORS=0
echo "Validating typer-patterns skill..."
echo "========================================"
# Check SKILL.md exists
echo "Checking SKILL.md..."
if [ ! -f "$SKILL_DIR/SKILL.md" ]; then
echo -e "${RED}✗ SKILL.md not found${NC}"
((ERRORS++))
else
echo -e "${GREEN}✓ SKILL.md exists${NC}"
# Check frontmatter starts at line 1
FIRST_LINE=$(head -n 1 "$SKILL_DIR/SKILL.md")
if [ "$FIRST_LINE" != "---" ]; then
echo -e "${RED}✗ SKILL.md frontmatter must start at line 1 (found: $FIRST_LINE)${NC}"
((ERRORS++))
else
echo -e "${GREEN}✓ Frontmatter starts at line 1${NC}"
fi
# Check required frontmatter fields
if grep -q "^name: " "$SKILL_DIR/SKILL.md"; then
echo -e "${GREEN}✓ name field present${NC}"
else
echo -e "${RED}✗ name field missing${NC}"
((ERRORS++))
fi
if grep -q "^description: " "$SKILL_DIR/SKILL.md"; then
echo -e "${GREEN}✓ description field present${NC}"
else
echo -e "${RED}✗ description field missing${NC}"
((ERRORS++))
fi
# Check for "Use when" in description
if grep "^description: " "$SKILL_DIR/SKILL.md" | grep -q "Use when"; then
echo -e "${GREEN}✓ Description contains 'Use when' triggers${NC}"
else
echo -e "${YELLOW}⚠ Description should include 'Use when' trigger contexts${NC}"
fi
fi
# Check templates directory
echo "Checking templates..."
TEMPLATE_COUNT=$(find "$SKILL_DIR/templates" -name "*.py" 2>/dev/null | wc -l)
if [ "$TEMPLATE_COUNT" -ge 4 ]; then
echo -e "${GREEN}✓ Found $TEMPLATE_COUNT templates (minimum 4)${NC}"
else
echo -e "${RED}✗ Found $TEMPLATE_COUNT templates (need at least 4)${NC}"
((ERRORS++))
fi
# Check scripts directory
echo "Checking scripts..."
SCRIPT_COUNT=$(find "$SKILL_DIR/scripts" -name "*.sh" 2>/dev/null | wc -l)
if [ "$SCRIPT_COUNT" -ge 3 ]; then
echo -e "${GREEN}✓ Found $SCRIPT_COUNT scripts (minimum 3)${NC}"
else
echo -e "${RED}✗ Found $SCRIPT_COUNT scripts (need at least 3)${NC}"
((ERRORS++))
fi
# Check scripts are executable
NONEXEC=$(find "$SKILL_DIR/scripts" -name "*.sh" ! -executable 2>/dev/null | wc -l)
if [ "$NONEXEC" -gt 0 ]; then
echo -e "${YELLOW}$NONEXEC scripts are not executable${NC}"
else
echo -e "${GREEN}✓ All scripts are executable${NC}"
fi
# Check examples directory
echo "Checking examples..."
EXAMPLE_COUNT=$(find "$SKILL_DIR/examples" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
if [ "$EXAMPLE_COUNT" -ge 3 ]; then
echo -e "${GREEN}✓ Found $EXAMPLE_COUNT example directories (minimum 3)${NC}"
else
echo -e "${RED}✗ Found $EXAMPLE_COUNT examples (need at least 3)${NC}"
((ERRORS++))
fi
# Check for README files in examples
for example_dir in "$SKILL_DIR/examples"/*; do
if [ -d "$example_dir" ]; then
example_name=$(basename "$example_dir")
if [ -f "$example_dir/README.md" ]; then
echo -e "${GREEN}✓ Example $example_name has README.md${NC}"
else
echo -e "${YELLOW}⚠ Example $example_name missing README.md${NC}"
fi
fi
done
# Check for hardcoded secrets (basic check)
echo "Checking for hardcoded secrets..."
if grep -r "sk-[a-zA-Z0-9]" "$SKILL_DIR" 2>/dev/null | grep -v "validate-skill.sh" | grep -q .; then
echo -e "${RED}✗ Possible API keys detected${NC}"
((ERRORS++))
else
echo -e "${GREEN}✓ No obvious API keys detected${NC}"
fi
# Check SKILL.md length
if [ -f "$SKILL_DIR/SKILL.md" ]; then
LINE_COUNT=$(wc -l < "$SKILL_DIR/SKILL.md")
if [ "$LINE_COUNT" -gt 150 ]; then
echo -e "${YELLOW}⚠ SKILL.md is $LINE_COUNT lines (consider keeping under 150)${NC}"
else
echo -e "${GREEN}✓ SKILL.md length is reasonable ($LINE_COUNT lines)${NC}"
fi
fi
echo "========================================"
if [ $ERRORS -eq 0 ]; then
echo -e "${GREEN}✓ Validation passed!${NC}"
exit 0
else
echo -e "${RED}✗ Validation failed with $ERRORS error(s)${NC}"
exit 1
fi

View File

@@ -0,0 +1,123 @@
#!/bin/bash
# Validate type hints in Typer CLI files
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check if file provided
if [ $# -eq 0 ]; then
echo -e "${RED}✗ Usage: $0 <python-file>${NC}"
exit 1
fi
FILE="$1"
# Check if file exists
if [ ! -f "$FILE" ]; then
echo -e "${RED}✗ File not found: $FILE${NC}"
exit 1
fi
echo "Validating type hints in: $FILE"
echo "----------------------------------------"
ERRORS=0
# Check for type hints on function parameters
echo "Checking function parameter type hints..."
UNTYPED_PARAMS=$(grep -n "def " "$FILE" | while read -r line; do
LINE_NUM=$(echo "$line" | cut -d: -f1)
LINE_CONTENT=$(echo "$line" | cut -d: -f2-)
# Extract parameter list
PARAMS=$(echo "$LINE_CONTENT" | sed -n 's/.*def [^(]*(\(.*\)).*/\1/p')
# Check if any parameter lacks type hint (excluding self, ctx)
if echo "$PARAMS" | grep -qE '[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*=' | \
grep -vE '[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*:[[:space:]]*[a-zA-Z]'; then
echo " Line $LINE_NUM: Missing type hint"
((ERRORS++))
fi
done)
if [ -z "$UNTYPED_PARAMS" ]; then
echo -e "${GREEN}✓ All parameters have type hints${NC}"
else
echo -e "${RED}$UNTYPED_PARAMS${NC}"
fi
# Check for return type hints
echo "Checking function return type hints..."
MISSING_RETURN=$(grep -n "def " "$FILE" | grep -v "-> " | while read -r line; do
LINE_NUM=$(echo "$line" | cut -d: -f1)
echo " Line $LINE_NUM: Missing return type hint"
((ERRORS++))
done)
if [ -z "$MISSING_RETURN" ]; then
echo -e "${GREEN}✓ All functions have return type hints${NC}"
else
echo -e "${RED}$MISSING_RETURN${NC}"
fi
# Check for Typer imports
echo "Checking Typer imports..."
if ! grep -q "^import typer" "$FILE" && ! grep -q "^from typer import" "$FILE"; then
echo -e "${RED}✗ Missing typer import${NC}"
((ERRORS++))
else
echo -e "${GREEN}✓ Typer imported${NC}"
fi
# Check for typing imports when using Optional, Union, etc.
echo "Checking typing imports..."
if grep -qE "Optional|Union|List|Dict|Tuple" "$FILE"; then
if ! grep -q "from typing import" "$FILE"; then
echo -e "${YELLOW}⚠ Using typing types but missing typing import${NC}"
((ERRORS++))
else
echo -e "${GREEN}✓ Typing imports present${NC}"
fi
else
echo -e "${YELLOW}⚠ No typing annotations detected${NC}"
fi
# Check for Path usage
echo "Checking Path usage for file parameters..."
if grep -qE "file|path|dir" "$FILE" | grep -i "str.*=.*typer"; then
echo -e "${YELLOW}⚠ Consider using Path type instead of str for file/path parameters${NC}"
else
echo -e "${GREEN}✓ No obvious Path type issues${NC}"
fi
# Check for Enum usage
echo "Checking for Enum patterns..."
if grep -qE "class.*\(str, Enum\)" "$FILE"; then
echo -e "${GREEN}✓ Enum classes found${NC}"
else
echo -e "${YELLOW}⚠ No Enum classes detected (consider for constrained choices)${NC}"
fi
# Check for docstrings
echo "Checking command docstrings..."
MISSING_DOCS=$(grep -A1 "def " "$FILE" | grep -v '"""' | wc -l)
if [ "$MISSING_DOCS" -gt 0 ]; then
echo -e "${YELLOW}⚠ Some functions may be missing docstrings${NC}"
else
echo -e "${GREEN}✓ Docstrings appear present${NC}"
fi
echo "----------------------------------------"
if [ $ERRORS -eq 0 ]; then
echo -e "${GREEN}✓ Validation passed!${NC}"
exit 0
else
echo -e "${RED}✗ Validation failed with $ERRORS error(s)${NC}"
exit 1
fi

View File

@@ -0,0 +1,233 @@
"""Advanced validation and callbacks template.
This template demonstrates:
- Custom validators with callbacks
- Complex validation logic
- Interdependent parameter validation
- Rich error messages
"""
import typer
from typing import Optional
from pathlib import Path
import re
app = typer.Typer()
# Custom validators
def validate_email(value: str) -> str:
"""Validate email format."""
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if not re.match(pattern, value):
raise typer.BadParameter("Invalid email format")
return value
def validate_port(value: int) -> int:
"""Validate port number range."""
if not 1024 <= value <= 65535:
raise typer.BadParameter("Port must be between 1024-65535")
return value
def validate_path_exists(value: Path) -> Path:
"""Validate that path exists."""
if not value.exists():
raise typer.BadParameter(f"Path does not exist: {value}")
return value
def validate_percentage(value: float) -> float:
"""Validate percentage range."""
if not 0.0 <= value <= 100.0:
raise typer.BadParameter("Percentage must be between 0-100")
return value
def validate_url(value: str) -> str:
"""Validate URL format."""
pattern = r"^https?://[^\s/$.?#].[^\s]*$"
if not re.match(pattern, value):
raise typer.BadParameter("Invalid URL format (must start with http:// or https://)")
return value
# Context manager for complex validation
class ValidationContext:
"""Context for cross-parameter validation."""
def __init__(self) -> None:
self.params: dict = {}
def add(self, key: str, value: any) -> None:
"""Add parameter to context."""
self.params[key] = value
def validate_dependencies(self) -> None:
"""Validate parameter dependencies."""
# Example: if ssl is enabled, cert and key must be provided
if self.params.get("ssl") and not (
self.params.get("cert") and self.params.get("key")
):
raise typer.BadParameter("SSL requires both --cert and --key")
# Global validation context
validation_context = ValidationContext()
@app.command()
def server(
host: str = typer.Option(
"127.0.0.1",
"--host",
"-h",
help="Server host",
),
port: int = typer.Option(
8000,
"--port",
"-p",
help="Server port",
callback=lambda _, value: validate_port(value),
),
ssl: bool = typer.Option(
False,
"--ssl",
help="Enable SSL/TLS",
),
cert: Optional[Path] = typer.Option(
None,
"--cert",
help="SSL certificate file",
callback=lambda _, value: validate_path_exists(value) if value else None,
),
key: Optional[Path] = typer.Option(
None,
"--key",
help="SSL private key file",
callback=lambda _, value: validate_path_exists(value) if value else None,
),
) -> None:
"""Start server with validated parameters.
Example:
$ python cli.py server --port 8443 --ssl --cert cert.pem --key key.pem
"""
# Store params for cross-validation
validation_context.add("ssl", ssl)
validation_context.add("cert", cert)
validation_context.add("key", key)
# Validate dependencies
try:
validation_context.validate_dependencies()
except typer.BadParameter as e:
typer.secho(f"✗ Validation error: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
# Start server
protocol = "https" if ssl else "http"
typer.echo(f"Starting server at {protocol}://{host}:{port}")
@app.command()
def user_create(
username: str = typer.Argument(..., help="Username (alphanumeric only)"),
email: str = typer.Option(
...,
"--email",
"-e",
help="User email",
callback=lambda _, value: validate_email(value),
),
age: Optional[int] = typer.Option(
None,
"--age",
help="User age",
min=13,
max=120,
),
) -> None:
"""Create user with validated inputs.
Example:
$ python cli.py user-create john --email john@example.com --age 25
"""
# Additional username validation
if not username.isalnum():
typer.secho(
"✗ Username must be alphanumeric", fg=typer.colors.RED, err=True
)
raise typer.Exit(1)
typer.secho(f"✓ User created: {username}", fg=typer.colors.GREEN)
@app.command()
def deploy(
url: str = typer.Option(
...,
"--url",
help="Deployment URL",
callback=lambda _, value: validate_url(value),
),
threshold: float = typer.Option(
95.0,
"--threshold",
help="Success threshold percentage",
callback=lambda _, value: validate_percentage(value),
),
rollback_on_error: bool = typer.Option(
True, "--rollback/--no-rollback", help="Rollback on error"
),
) -> None:
"""Deploy with validated URL and threshold.
Example:
$ python cli.py deploy --url https://example.com --threshold 99.5
"""
typer.echo(f"Deploying to: {url}")
typer.echo(f"Success threshold: {threshold}%")
typer.echo(f"Rollback on error: {rollback_on_error}")
@app.command()
def batch_process(
input_dir: Path = typer.Argument(
...,
help="Input directory",
callback=lambda _, value: validate_path_exists(value),
),
pattern: str = typer.Option(
"*.txt", "--pattern", "-p", help="File pattern"
),
workers: int = typer.Option(
4,
"--workers",
"-w",
help="Number of worker threads",
min=1,
max=32,
),
) -> None:
"""Batch process files with validation.
Example:
$ python cli.py batch-process ./data --pattern "*.json" --workers 8
"""
if not input_dir.is_dir():
typer.secho(
f"✗ Not a directory: {input_dir}", fg=typer.colors.RED, err=True
)
raise typer.Exit(1)
typer.echo(f"Processing files in: {input_dir}")
typer.echo(f"Pattern: {pattern}")
typer.echo(f"Workers: {workers}")
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,68 @@
"""Basic type-safe Typer command template.
This template demonstrates modern Typer usage with:
- Full type hints on all parameters
- Path type for file operations
- Optional parameters with defaults
- Typed return hints
"""
import typer
from pathlib import Path
from typing import Optional
app = typer.Typer()
@app.command()
def process(
input_file: Path = typer.Argument(
...,
help="Input file to process",
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
),
output_file: Optional[Path] = typer.Option(
None,
"--output",
"-o",
help="Output file path (optional)",
),
verbose: bool = typer.Option(
False,
"--verbose",
"-v",
help="Enable verbose output",
),
count: int = typer.Option(
10,
"--count",
"-c",
help="Number of items to process",
min=1,
max=1000,
),
) -> None:
"""Process input file with type-safe parameters.
Example:
$ python cli.py input.txt --output result.txt --verbose --count 50
"""
if verbose:
typer.echo(f"Processing {input_file}")
typer.echo(f"Count: {count}")
# Your processing logic here
content = input_file.read_text()
if output_file:
output_file.write_text(content)
typer.secho(f"✓ Saved to {output_file}", fg=typer.colors.GREEN)
else:
typer.echo(content)
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,100 @@
"""Enum-based options template for Typer.
This template demonstrates:
- Enum usage for constrained choices
- Multiple enum types
- Enum with autocomplete
- Type-safe enum handling
"""
import typer
from enum import Enum
from typing import Optional
class LogLevel(str, Enum):
"""Log level choices."""
debug = "debug"
info = "info"
warning = "warning"
error = "error"
class OutputFormat(str, Enum):
"""Output format choices."""
json = "json"
yaml = "yaml"
text = "text"
csv = "csv"
class Environment(str, Enum):
"""Deployment environment choices."""
development = "development"
staging = "staging"
production = "production"
app = typer.Typer()
@app.command()
def deploy(
environment: Environment = typer.Argument(
..., help="Target deployment environment"
),
format: OutputFormat = typer.Option(
OutputFormat.json, "--format", "-f", help="Output format for logs"
),
log_level: LogLevel = typer.Option(
LogLevel.info, "--log-level", "-l", help="Logging level"
),
force: bool = typer.Option(False, "--force", help="Force deployment"),
) -> None:
"""Deploy application with enum-based options.
Example:
$ python cli.py production --format yaml --log-level debug
"""
typer.echo(f"Deploying to: {environment.value}")
typer.echo(f"Output format: {format.value}")
typer.echo(f"Log level: {log_level.value}")
if force:
typer.secho("⚠ Force deployment enabled", fg=typer.colors.YELLOW)
# Deployment logic here
# The enum values are guaranteed to be valid
@app.command()
def export(
format: OutputFormat = typer.Argument(
OutputFormat.json, help="Export format"
),
output: Optional[str] = typer.Option(None, "--output", "-o"),
) -> None:
"""Export data in specified format.
Example:
$ python cli.py export yaml --output data.yaml
"""
typer.echo(f"Exporting as {format.value}")
# Export logic based on format
match format:
case OutputFormat.json:
typer.echo("Generating JSON...")
case OutputFormat.yaml:
typer.echo("Generating YAML...")
case OutputFormat.text:
typer.echo("Generating plain text...")
case OutputFormat.csv:
typer.echo("Generating CSV...")
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,164 @@
"""Sub-application structure template.
This template demonstrates:
- Multiple sub-apps for command organization
- Shared context between commands
- Hierarchical command structure
- Clean separation of concerns
"""
import typer
from typing import Optional
from pathlib import Path
# Main application
app = typer.Typer(
name="mycli",
help="Example CLI with sub-applications",
add_completion=True,
)
# Database sub-app
db_app = typer.Typer(help="Database management commands")
app.add_typer(db_app, name="db")
# Server sub-app
server_app = typer.Typer(help="Server management commands")
app.add_typer(server_app, name="server")
# User sub-app
user_app = typer.Typer(help="User management commands")
app.add_typer(user_app, name="user")
# Main app callback for global options
@app.callback()
def main(
ctx: typer.Context,
config: Optional[Path] = typer.Option(
None, "--config", "-c", help="Config file path"
),
verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
"""Global options for all commands."""
# Store in context for sub-commands
ctx.obj = {"config": config, "verbose": verbose}
if verbose:
typer.echo(f"Config: {config or 'default'}")
# Database commands
@db_app.command("migrate")
def db_migrate(
ctx: typer.Context,
direction: str = typer.Argument("up", help="Migration direction: up/down"),
steps: int = typer.Option(1, help="Number of migration steps"),
) -> None:
"""Run database migrations."""
verbose = ctx.obj.get("verbose", False)
if verbose:
typer.echo(f"Running {steps} migration(s) {direction}")
typer.secho("✓ Migrations complete", fg=typer.colors.GREEN)
@db_app.command("seed")
def db_seed(
ctx: typer.Context, file: Optional[Path] = typer.Option(None, "--file", "-f")
) -> None:
"""Seed database with test data."""
verbose = ctx.obj.get("verbose", False)
if verbose:
typer.echo(f"Seeding from: {file or 'default seed'}")
typer.secho("✓ Database seeded", fg=typer.colors.GREEN)
@db_app.command("backup")
def db_backup(ctx: typer.Context, output: Path = typer.Argument(...)) -> None:
"""Backup database to file."""
typer.echo(f"Backing up database to {output}")
typer.secho("✓ Backup complete", fg=typer.colors.GREEN)
# Server commands
@server_app.command("start")
def server_start(
ctx: typer.Context,
port: int = typer.Option(8000, help="Server port"),
host: str = typer.Option("127.0.0.1", help="Server host"),
reload: bool = typer.Option(False, "--reload", help="Enable auto-reload"),
) -> None:
"""Start the application server."""
verbose = ctx.obj.get("verbose", False)
if verbose:
typer.echo(f"Starting server on {host}:{port}")
if reload:
typer.echo("Auto-reload enabled")
typer.secho("✓ Server started", fg=typer.colors.GREEN)
@server_app.command("stop")
def server_stop(ctx: typer.Context) -> None:
"""Stop the application server."""
typer.echo("Stopping server...")
typer.secho("✓ Server stopped", fg=typer.colors.GREEN)
@server_app.command("status")
def server_status(ctx: typer.Context) -> None:
"""Check server status."""
typer.echo("Server status: Running")
# User commands
@user_app.command("create")
def user_create(
ctx: typer.Context,
username: str = typer.Argument(..., help="Username"),
email: str = typer.Argument(..., help="Email address"),
admin: bool = typer.Option(False, "--admin", help="Create as admin"),
) -> None:
"""Create a new user."""
verbose = ctx.obj.get("verbose", False)
if verbose:
typer.echo(f"Creating user: {username} ({email})")
if admin:
typer.echo("Creating with admin privileges")
typer.secho(f"✓ User {username} created", fg=typer.colors.GREEN)
@user_app.command("delete")
def user_delete(
ctx: typer.Context,
username: str = typer.Argument(..., help="Username"),
force: bool = typer.Option(False, "--force", help="Force deletion"),
) -> None:
"""Delete a user."""
if not force:
confirm = typer.confirm(f"Delete user {username}?")
if not confirm:
typer.echo("Cancelled")
raise typer.Abort()
typer.secho(f"✓ User {username} deleted", fg=typer.colors.RED)
@user_app.command("list")
def user_list(ctx: typer.Context) -> None:
"""List all users."""
typer.echo("Listing users...")
# List logic here
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,143 @@
"""Typer instance factory pattern template.
This template demonstrates:
- Factory function for creating Typer apps
- Better testability
- Configuration injection
- Dependency management
"""
import typer
from typing import Optional, Protocol
from pathlib import Path
from dataclasses import dataclass
# Configuration
@dataclass
class Config:
"""Application configuration."""
verbose: bool = False
debug: bool = False
config_file: Optional[Path] = None
# Service protocol (dependency injection)
class StorageService(Protocol):
"""Storage service interface."""
def save(self, data: str) -> None:
"""Save data."""
...
def load(self) -> str:
"""Load data."""
...
class FileStorage:
"""File-based storage implementation."""
def __init__(self, base_path: Path) -> None:
self.base_path = base_path
def save(self, data: str) -> None:
"""Save data to file."""
self.base_path.write_text(data)
def load(self) -> str:
"""Load data from file."""
return self.base_path.read_text()
def create_app(
config: Optional[Config] = None, storage: Optional[StorageService] = None
) -> typer.Typer:
"""Factory function for creating Typer application.
This pattern allows for:
- Easy testing with mocked dependencies
- Configuration injection
- Multiple app instances with different configs
Args:
config: Application configuration
storage: Storage service implementation
Returns:
Configured Typer application
"""
config = config or Config()
storage = storage or FileStorage(Path("data.txt"))
app = typer.Typer(
name="myapp",
help="Example CLI with factory pattern",
add_completion=True,
no_args_is_help=True,
rich_markup_mode="rich",
)
@app.command()
def save(
data: str = typer.Argument(..., help="Data to save"),
force: bool = typer.Option(False, "--force", help="Overwrite existing"),
) -> None:
"""Save data using injected storage."""
if config.verbose:
typer.echo(f"Saving: {data}")
try:
storage.save(data)
typer.secho("✓ Data saved successfully", fg=typer.colors.GREEN)
except Exception as e:
if config.debug:
raise
typer.secho(f"✗ Error: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
@app.command()
def load() -> None:
"""Load data using injected storage."""
try:
data = storage.load()
typer.echo(data)
except FileNotFoundError:
typer.secho("✗ No data found", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
except Exception as e:
if config.debug:
raise
typer.secho(f"✗ Error: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
@app.command()
def status() -> None:
"""Show application status."""
typer.echo("Application Status:")
typer.echo(f" Verbose: {config.verbose}")
typer.echo(f" Debug: {config.debug}")
typer.echo(f" Config: {config.config_file or 'default'}")
return app
def main() -> None:
"""Main entry point with configuration setup."""
# Parse global options
import sys
verbose = "--verbose" in sys.argv or "-v" in sys.argv
debug = "--debug" in sys.argv
# Create configuration
config = Config(verbose=verbose, debug=debug)
# Create and run app
app = create_app(config=config)
app()
if __name__ == "__main__":
main()