Initial commit
This commit is contained in:
145
skills/typer-patterns/QUICKSTART.md
Normal file
145
skills/typer-patterns/QUICKSTART.md
Normal 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
|
||||
201
skills/typer-patterns/SKILL.md
Normal file
201
skills/typer-patterns/SKILL.md
Normal 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/*/`
|
||||
50
skills/typer-patterns/examples/basic-cli/README.md
Normal file
50
skills/typer-patterns/examples/basic-cli/README.md
Normal 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
|
||||
43
skills/typer-patterns/examples/basic-cli/cli.py
Normal file
43
skills/typer-patterns/examples/basic-cli/cli.py
Normal 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()
|
||||
66
skills/typer-patterns/examples/enum-cli/README.md
Normal file
66
skills/typer-patterns/examples/enum-cli/README.md
Normal 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
|
||||
102
skills/typer-patterns/examples/enum-cli/cli.py
Normal file
102
skills/typer-patterns/examples/enum-cli/cli.py
Normal 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()
|
||||
128
skills/typer-patterns/examples/factory-cli/README.md
Normal file
128
skills/typer-patterns/examples/factory-cli/README.md
Normal 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)
|
||||
```
|
||||
162
skills/typer-patterns/examples/factory-cli/cli.py
Normal file
162
skills/typer-patterns/examples/factory-cli/cli.py
Normal 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()
|
||||
77
skills/typer-patterns/examples/subapp-cli/README.md
Normal file
77
skills/typer-patterns/examples/subapp-cli/README.md
Normal 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
|
||||
132
skills/typer-patterns/examples/subapp-cli/cli.py
Normal file
132
skills/typer-patterns/examples/subapp-cli/cli.py
Normal 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()
|
||||
105
skills/typer-patterns/scripts/convert-argparse.sh
Executable file
105
skills/typer-patterns/scripts/convert-argparse.sh
Executable 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}"
|
||||
113
skills/typer-patterns/scripts/generate-cli.sh
Executable file
113
skills/typer-patterns/scripts/generate-cli.sh
Executable 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"
|
||||
138
skills/typer-patterns/scripts/test-cli.sh
Executable file
138
skills/typer-patterns/scripts/test-cli.sh
Executable 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
|
||||
135
skills/typer-patterns/scripts/validate-skill.sh
Executable file
135
skills/typer-patterns/scripts/validate-skill.sh
Executable 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
|
||||
123
skills/typer-patterns/scripts/validate-types.sh
Executable file
123
skills/typer-patterns/scripts/validate-types.sh
Executable 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
|
||||
233
skills/typer-patterns/templates/advanced-validation.py
Normal file
233
skills/typer-patterns/templates/advanced-validation.py
Normal 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()
|
||||
68
skills/typer-patterns/templates/basic-typed-command.py
Normal file
68
skills/typer-patterns/templates/basic-typed-command.py
Normal 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()
|
||||
100
skills/typer-patterns/templates/enum-options.py
Normal file
100
skills/typer-patterns/templates/enum-options.py
Normal 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()
|
||||
164
skills/typer-patterns/templates/sub-app-structure.py
Normal file
164
skills/typer-patterns/templates/sub-app-structure.py
Normal 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()
|
||||
143
skills/typer-patterns/templates/typer-instance.py
Normal file
143
skills/typer-patterns/templates/typer-instance.py
Normal 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()
|
||||
Reference in New Issue
Block a user