Initial commit
This commit is contained in:
473
skills/argparse-patterns/examples/advanced-parsing.md
Normal file
473
skills/argparse-patterns/examples/advanced-parsing.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# Advanced argparse Patterns
|
||||
|
||||
Complex argument parsing scenarios and advanced techniques.
|
||||
|
||||
## Templates Reference
|
||||
|
||||
- `templates/custom-actions.py`
|
||||
- `templates/mutually-exclusive.py`
|
||||
- `templates/argument-groups.py`
|
||||
- `templates/variadic-args.py`
|
||||
|
||||
## Overview
|
||||
|
||||
Advanced patterns:
|
||||
- Custom action classes
|
||||
- Mutually exclusive groups
|
||||
- Argument groups (organization)
|
||||
- Variadic arguments (nargs)
|
||||
- Environment variable fallback
|
||||
- Config file integration
|
||||
- Subparser inheritance
|
||||
|
||||
## 1. Custom Actions
|
||||
|
||||
Create custom argument processing logic.
|
||||
|
||||
### Simple Custom Action
|
||||
|
||||
```python
|
||||
class UpperCaseAction(argparse.Action):
|
||||
"""Convert value to uppercase."""
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
setattr(namespace, self.dest, values.upper())
|
||||
|
||||
|
||||
parser.add_argument('--name', action=UpperCaseAction)
|
||||
```
|
||||
|
||||
### Key-Value Action
|
||||
|
||||
```python
|
||||
class KeyValueAction(argparse.Action):
|
||||
"""Parse key=value pairs."""
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
if '=' not in values:
|
||||
parser.error(f"Must be key=value format: {values}")
|
||||
|
||||
key, value = values.split('=', 1)
|
||||
items = getattr(namespace, self.dest, {}) or {}
|
||||
items[key] = value
|
||||
setattr(namespace, self.dest, items)
|
||||
|
||||
|
||||
parser.add_argument(
|
||||
'--env', '-e',
|
||||
action=KeyValueAction,
|
||||
help='Environment variable (key=value)'
|
||||
)
|
||||
|
||||
# Usage: --env API_KEY=abc123 --env DB_URL=postgres://...
|
||||
```
|
||||
|
||||
### Load File Action
|
||||
|
||||
```python
|
||||
class LoadFileAction(argparse.Action):
|
||||
"""Load and parse file content."""
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
try:
|
||||
with open(values, 'r') as f:
|
||||
content = f.read()
|
||||
setattr(namespace, self.dest, content)
|
||||
except Exception as e:
|
||||
parser.error(f"Cannot load file {values}: {e}")
|
||||
|
||||
|
||||
parser.add_argument('--config', action=LoadFileAction)
|
||||
```
|
||||
|
||||
## 2. Mutually Exclusive Groups
|
||||
|
||||
Ensure only one option from a group is used.
|
||||
|
||||
### Basic Exclusivity
|
||||
|
||||
```python
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--json', help='Output as JSON')
|
||||
group.add_argument('--yaml', help='Output as YAML')
|
||||
group.add_argument('--xml', help='Output as XML')
|
||||
|
||||
# Valid: --json output.json
|
||||
# Valid: --yaml output.yaml
|
||||
# Invalid: --json output.json --yaml output.yaml
|
||||
```
|
||||
|
||||
### Required Exclusive Group
|
||||
|
||||
```python
|
||||
mode_group = parser.add_mutually_exclusive_group(required=True)
|
||||
mode_group.add_argument('--create', metavar='NAME')
|
||||
mode_group.add_argument('--update', metavar='NAME')
|
||||
mode_group.add_argument('--delete', metavar='NAME')
|
||||
mode_group.add_argument('--list', action='store_true')
|
||||
|
||||
# Must specify exactly one: create, update, delete, or list
|
||||
```
|
||||
|
||||
### Multiple Exclusive Groups
|
||||
|
||||
```python
|
||||
# Output format group
|
||||
output = parser.add_mutually_exclusive_group()
|
||||
output.add_argument('--json', action='store_true')
|
||||
output.add_argument('--yaml', action='store_true')
|
||||
|
||||
# Verbosity group
|
||||
verbosity = parser.add_mutually_exclusive_group()
|
||||
verbosity.add_argument('--verbose', action='store_true')
|
||||
verbosity.add_argument('--quiet', action='store_true')
|
||||
|
||||
# Can use one from each group:
|
||||
# Valid: --json --verbose
|
||||
# Invalid: --json --yaml
|
||||
```
|
||||
|
||||
## 3. Argument Groups
|
||||
|
||||
Organize arguments for better help display.
|
||||
|
||||
```python
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
# Server configuration
|
||||
server_group = parser.add_argument_group(
|
||||
'server configuration',
|
||||
'Options for configuring the web server'
|
||||
)
|
||||
server_group.add_argument('--host', default='127.0.0.1')
|
||||
server_group.add_argument('--port', type=int, default=8080)
|
||||
server_group.add_argument('--workers', type=int, default=4)
|
||||
|
||||
# Database configuration
|
||||
db_group = parser.add_argument_group(
|
||||
'database configuration',
|
||||
'Options for database connection'
|
||||
)
|
||||
db_group.add_argument('--db-host', default='localhost')
|
||||
db_group.add_argument('--db-port', type=int, default=5432)
|
||||
db_group.add_argument('--db-name', required=True)
|
||||
|
||||
# Logging configuration
|
||||
log_group = parser.add_argument_group(
|
||||
'logging configuration',
|
||||
'Options for logging and monitoring'
|
||||
)
|
||||
log_group.add_argument('--log-level',
|
||||
choices=['debug', 'info', 'warning', 'error'],
|
||||
default='info')
|
||||
log_group.add_argument('--log-file', help='Log to file')
|
||||
```
|
||||
|
||||
**Help output groups arguments logically.**
|
||||
|
||||
## 4. Variadic Arguments (nargs)
|
||||
|
||||
Handle variable number of arguments.
|
||||
|
||||
### Optional Single Argument (?)
|
||||
|
||||
```python
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
nargs='?',
|
||||
const='default.json', # Used if flag present, no value
|
||||
default=None, # Used if flag not present
|
||||
help='Output file'
|
||||
)
|
||||
|
||||
# --output → 'default.json'
|
||||
# --output file.json → 'file.json'
|
||||
# (no flag) → None
|
||||
```
|
||||
|
||||
### Zero or More (*)
|
||||
|
||||
```python
|
||||
parser.add_argument(
|
||||
'--include',
|
||||
nargs='*',
|
||||
default=[],
|
||||
help='Include patterns'
|
||||
)
|
||||
|
||||
# --include → []
|
||||
# --include *.py → ['*.py']
|
||||
# --include *.py *.md → ['*.py', '*.md']
|
||||
```
|
||||
|
||||
### One or More (+)
|
||||
|
||||
```python
|
||||
parser.add_argument(
|
||||
'files',
|
||||
nargs='+',
|
||||
help='Input files (at least one required)'
|
||||
)
|
||||
|
||||
# file1.txt → ['file1.txt']
|
||||
# file1.txt file2.txt → ['file1.txt', 'file2.txt']
|
||||
# (no files) → Error: required
|
||||
```
|
||||
|
||||
### Exact Number
|
||||
|
||||
```python
|
||||
parser.add_argument(
|
||||
'--range',
|
||||
nargs=2,
|
||||
type=int,
|
||||
metavar=('START', 'END'),
|
||||
help='Range as start end'
|
||||
)
|
||||
|
||||
# --range 1 10 → [1, 10]
|
||||
# --range 1 → Error: expected 2
|
||||
```
|
||||
|
||||
### Remainder
|
||||
|
||||
```python
|
||||
parser.add_argument(
|
||||
'--command',
|
||||
nargs=argparse.REMAINDER,
|
||||
help='Pass-through command and args'
|
||||
)
|
||||
|
||||
# mycli --command python script.py --arg1 --arg2
|
||||
# → command = ['python', 'script.py', '--arg1', '--arg2']
|
||||
```
|
||||
|
||||
## 5. Environment Variable Fallback
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
'--api-key',
|
||||
default=os.environ.get('API_KEY'),
|
||||
help='API key (default: $API_KEY)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--db-url',
|
||||
default=os.environ.get('DATABASE_URL'),
|
||||
help='Database URL (default: $DATABASE_URL)'
|
||||
)
|
||||
|
||||
# Precedence: CLI arg > Environment variable > Default
|
||||
```
|
||||
|
||||
## 6. Config File Integration
|
||||
|
||||
```python
|
||||
import configparser
|
||||
|
||||
def load_config(config_file):
|
||||
"""Load configuration from INI file."""
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_file)
|
||||
return config
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument('--config', help='Config file')
|
||||
parser.add_argument('--host', help='Server host')
|
||||
parser.add_argument('--port', type=int, help='Server port')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load config file if specified
|
||||
if args.config:
|
||||
config = load_config(args.config)
|
||||
|
||||
# Use config values as defaults if not specified on CLI
|
||||
if not args.host:
|
||||
args.host = config.get('server', 'host', fallback='127.0.0.1')
|
||||
|
||||
if not args.port:
|
||||
args.port = config.getint('server', 'port', fallback=8080)
|
||||
```
|
||||
|
||||
## 7. Parent Parsers (Inheritance)
|
||||
|
||||
Share common arguments across subcommands.
|
||||
|
||||
```python
|
||||
# Parent parser with common arguments
|
||||
parent_parser = argparse.ArgumentParser(add_help=False)
|
||||
parent_parser.add_argument('--verbose', action='store_true')
|
||||
parent_parser.add_argument('--config', help='Config file')
|
||||
|
||||
# Main parser
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='command')
|
||||
|
||||
# Subcommands inherit from parent
|
||||
deploy_parser = subparsers.add_parser(
|
||||
'deploy',
|
||||
parents=[parent_parser],
|
||||
help='Deploy application'
|
||||
)
|
||||
deploy_parser.add_argument('environment')
|
||||
|
||||
build_parser = subparsers.add_parser(
|
||||
'build',
|
||||
parents=[parent_parser],
|
||||
help='Build application'
|
||||
)
|
||||
build_parser.add_argument('--target')
|
||||
|
||||
# Both subcommands have --verbose and --config
|
||||
```
|
||||
|
||||
## 8. Argument Defaults from Dict
|
||||
|
||||
```python
|
||||
defaults = {
|
||||
'host': '127.0.0.1',
|
||||
'port': 8080,
|
||||
'workers': 4,
|
||||
'timeout': 30.0
|
||||
}
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--host')
|
||||
parser.add_argument('--port', type=int)
|
||||
parser.add_argument('--workers', type=int)
|
||||
parser.add_argument('--timeout', type=float)
|
||||
|
||||
# Set all defaults at once
|
||||
parser.set_defaults(**defaults)
|
||||
```
|
||||
|
||||
## 9. Namespace Manipulation
|
||||
|
||||
```python
|
||||
# Pre-populate namespace
|
||||
defaults = argparse.Namespace(
|
||||
host='127.0.0.1',
|
||||
port=8080,
|
||||
debug=False
|
||||
)
|
||||
|
||||
args = parser.parse_args(namespace=defaults)
|
||||
|
||||
# Or modify after parsing
|
||||
args = parser.parse_args()
|
||||
args.computed_value = args.value1 + args.value2
|
||||
```
|
||||
|
||||
## 10. Conditional Arguments
|
||||
|
||||
```python
|
||||
args = parser.parse_args()
|
||||
|
||||
# Add conditional validation
|
||||
if args.ssl and not (args.cert and args.key):
|
||||
parser.error("--ssl requires both --cert and --key")
|
||||
|
||||
# Add computed values
|
||||
if args.workers == 'auto':
|
||||
import os
|
||||
args.workers = os.cpu_count()
|
||||
```
|
||||
|
||||
## Complete Advanced Example
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
class KeyValueAction(argparse.Action):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
key, value = values.split('=', 1)
|
||||
items = getattr(namespace, self.dest, {}) or {}
|
||||
items[key] = value
|
||||
setattr(namespace, self.dest, items)
|
||||
|
||||
|
||||
def main():
|
||||
# Parent parser for common args
|
||||
parent = argparse.ArgumentParser(add_help=False)
|
||||
parent.add_argument('--verbose', action='store_true')
|
||||
parent.add_argument('--config', help='Config file')
|
||||
|
||||
# Main parser
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Advanced argparse patterns'
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest='command', required=True)
|
||||
|
||||
# Deploy command
|
||||
deploy = subparsers.add_parser(
|
||||
'deploy',
|
||||
parents=[parent],
|
||||
help='Deploy application'
|
||||
)
|
||||
|
||||
# Mutually exclusive group
|
||||
format_group = deploy.add_mutually_exclusive_group()
|
||||
format_group.add_argument('--json', action='store_true')
|
||||
format_group.add_argument('--yaml', action='store_true')
|
||||
|
||||
# Custom action
|
||||
deploy.add_argument(
|
||||
'--env', '-e',
|
||||
action=KeyValueAction,
|
||||
help='Environment variable'
|
||||
)
|
||||
|
||||
# Variadic arguments
|
||||
deploy.add_argument(
|
||||
'targets',
|
||||
nargs='+',
|
||||
help='Deployment targets'
|
||||
)
|
||||
|
||||
# Environment fallback
|
||||
deploy.add_argument(
|
||||
'--api-key',
|
||||
default=os.environ.get('API_KEY'),
|
||||
help='API key'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Post-parse validation
|
||||
if args.command == 'deploy':
|
||||
if not args.api_key:
|
||||
parser.error("API key required (use --api-key or $API_KEY)")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use parent parsers** for shared arguments
|
||||
2. **Use argument groups** for organization
|
||||
3. **Use mutually exclusive groups** when appropriate
|
||||
4. **Validate after parsing** for complex logic
|
||||
5. **Provide environment fallbacks** for sensitive data
|
||||
6. **Use custom actions** for complex transformations
|
||||
7. **Document nargs behavior** in help text
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Review template files for complete implementations
|
||||
- Test patterns with `scripts/test-parser.sh`
|
||||
- Compare with Click/Typer alternatives
|
||||
Reference in New Issue
Block a user