Supplementary: Making Your CLI Beautiful¶
Supplementary Material
This is bonus content that builds on Episode 2: Entry Points & CLI Tools. It shows how to add colors, tables, and formatted output to your command-line interface.
🎨 Why Beautiful CLIs Matter¶
Compare these two outputs:
Plain:
Enhanced:
The enhanced version is: - Easier to scan - symbols and colors help spot issues quickly - More professional - looks polished and well-maintained - User-friendly - clear visual hierarchy
📦 The rich Library¶
rich is a Python library for rich text and beautiful formatting in the terminal.
Features:
- ✅ Cross-platform color support (Windows, Mac, Linux)
- ✅ Tables, progress bars, syntax highlighting
- ✅ Panels, columns, and layout
- ✅ Automatic detection of terminal capabilities
Installation:
🔧 Adding rich to kir-pydemo¶
Step 1: Make It an Optional Dependency¶
Edit pyproject.toml:
Now users can install it with:
Step 2: Implement Graceful Degradation¶
The CLI should work with or without rich installed.
Pattern:
# At the top of cli.py
try:
from rich.console import Console
from rich.panel import Panel
HAS_RICH = True
console = Console()
except ImportError:
HAS_RICH = False
console = None
# Then use conditional logic
def print_success(message: str):
"""Print success message with color if available."""
if HAS_RICH:
console.print(f"[green]✓[/green] {message}")
else:
print(f"✓ {message}")
This way:
- ✅ Works great with
richinstalled - ✅ Still works fine without it (plain text)
- ✅ No crashes or ImportErrors
🎯 Practical Examples¶
Example 1: Colored Output¶
Update cli.py with helper functions:
"""Command-line interface for kir-pydemo."""
import argparse
import sys
from pathlib import Path
from kir_pydemo import gc_content, reverse_complement, __version__
# Try to import rich
try:
from rich.console import Console
HAS_RICH = True
console = Console()
except ImportError:
HAS_RICH = False
console = None
def print_success(message: str):
"""Print success message."""
if HAS_RICH:
console.print(f"[green]✓[/green] {message}")
else:
print(f"✓ {message}")
def print_error(message: str):
"""Print error message."""
if HAS_RICH:
console.print(f"[red]✗[/red] {message}", file=sys.stderr)
else:
print(f"✗ {message}", file=sys.stderr)
def print_result(label: str, value: str, highlight: bool = False):
"""Print a result value."""
if HAS_RICH:
if highlight:
console.print(f"{label}: [bold green]{value}[/bold green]")
else:
console.print(f"{label}: {value}")
else:
print(f"{label}: {value}")
Use in your commands:
def cmd_gc_content(args: argparse.Namespace) -> int:
"""Handle the gc-content command."""
# ... existing code to get sequences ...
try:
result = gc_content(sequence)
print_result("GC content", f"{result:.{args.precision}f}%", highlight=True)
return 0
except ValueError as e:
print_error(str(e))
return 1
Example 2: Banner¶
Add a colorful banner:
try:
from rich.console import Console
from rich.panel import Panel
from rich import box
HAS_RICH = True
console = Console()
except ImportError:
HAS_RICH = False
console = None
def print_banner():
"""Print a colorful banner."""
if HAS_RICH:
banner_text = f"""
[bold cyan]kir-pydemo[/bold cyan] v{__version__}
[dim]DNA Sequence Analysis Tools[/dim]
[yellow]Features:[/yellow]
• GC content calculation
• Reverse complement generation
• FASTA file support
"""
console.print(Panel(
banner_text,
box=box.ROUNDED,
border_style="cyan",
padding=(1, 2)
))
else:
# Plain fallback
print(f"\nkir-pydemo v{__version__}")
print("DNA Sequence Analysis Tools\n")
# Add --banner flag to parser
parser.add_argument(
"--banner",
action="store_true",
help="Show banner (requires rich)",
)
# In main()
def main() -> int:
parser = create_parser()
args = parser.parse_args()
if args.banner:
print_banner()
# ... rest of main
Usage:
Output (with rich):
╭─────────────────────────────────────╮
│ │
│ kir-pydemo v0.1.0 │
│ DNA Sequence Analysis Tools │
│ │
│ Features: │
│ • GC content calculation │
│ • Reverse complement generation │
│ • FASTA file support │
│ │
╰─────────────────────────────────────╯
Example 3: Tables for Multiple Results¶
When processing multiple sequences:
try:
from rich.console import Console
from rich.table import Table
from rich import box
HAS_RICH = True
console = Console()
except ImportError:
HAS_RICH = False
console = None
def print_results_table(results: list):
"""Print results in a table format."""
if HAS_RICH:
table = Table(title="GC Content Analysis", box=box.SIMPLE)
table.add_column("Sequence", style="cyan", no_wrap=False)
table.add_column("GC %", justify="right", style="green")
for seq, gc_val in results:
# Truncate long sequences
display_seq = seq[:30] + "..." if len(seq) > 30 else seq
# Color-code based on GC content
if gc_val < 40:
gc_style = "blue"
elif gc_val > 60:
gc_style = "red"
else:
gc_style = "green"
table.add_row(
display_seq,
f"[{gc_style}]{gc_val:.2f}[/{gc_style}]"
)
console.print(table)
else:
# Plain text fallback
print("\nGC Content Analysis")
print("-" * 50)
for seq, gc_val in results:
display_seq = seq[:30] + "..." if len(seq) > 30 else seq
print(f"{display_seq:35} {gc_val:6.2f}%")
print("-" * 50)
Add table option to parser:
gc_parser.add_argument(
"-t", "--table",
action="store_true",
help="Display results in table format",
)
Usage:
Output (with rich):
GC Content Analysis
┌────────────────────────┬────────┐
│ Sequence │ GC % │
├────────────────────────┼────────┤
│ ATGCATGC │ 50.00 │
│ AAAAAAAAAA │ 0.00 │
│ GGGGGGGGGG │ 100.00 │
└────────────────────────┴────────┘
Example 4: Progress Bar (Bonus)¶
For long-running operations:
from rich.progress import track
def process_fasta_file(filepath: Path):
"""Process FASTA file with progress bar."""
sequences = read_fasta(filepath)
if HAS_RICH:
results = []
for name, seq in track(sequences, description="Processing..."):
gc_val = gc_content(seq)
results.append((name, seq, gc_val))
return results
else:
# Plain version
results = []
for name, seq in sequences:
gc_val = gc_content(seq)
results.append((name, seq, gc_val))
print(f"Processed {name}")
return results
🎨 Rich Color Reference¶
Common color names:
- red, green, blue, yellow, cyan, magenta
- bright_red, bright_green, etc.
Text styles:
- [bold]text[/bold] - Bold
- [dim]text[/dim] - Dimmed
- [italic]text[/italic] - Italic
- [underline]text[/underline] - Underlined
Combinations:
📋 Best Practices¶
1. Always Provide Fallbacks¶
# ✅ Good - works with or without rich
if HAS_RICH:
console.print("[green]Success[/green]")
else:
print("Success")
# ❌ Bad - crashes without rich
console.print("[green]Success[/green]") # NameError if not installed
2. Don't Overuse Colors¶
# ✅ Good - colors have meaning
print_error("File not found") # Red
print_success("Done!") # Green
# ❌ Bad - rainbow soup
console.print("[red]The[/red] [blue]quick[/blue] [green]brown[/green]...")
3. Respect NO_COLOR Environment Variable¶
Rich automatically respects the NO_COLOR environment variable. Users can disable colors:
4. Test Without Rich¶
Always test that your CLI works without rich installed:
# Create a test environment without rich
python -m venv test-env
source test-env/bin/activate
pip install -e . # Without [cli-extras]
# Should still work, just without colors
kir-pydemo gc-content ATGC
🚀 Real-World Examples¶
Other popular tools using rich:
- Poetry - Python dependency management
- Typer - CLI framework (built on rich)
- HTTPie - HTTP client
- Pre-commit - Git hooks framework
📚 Further Reading¶
- Rich Documentation
- Rich Gallery - Examples
- Typer - CLI framework with rich integration
- Click Rich - Rich formatting for Click
✅ Summary¶
Adding rich to your CLI:
- Add as optional dependency -
[project.optional-dependencies] - Check if available -
try/except ImportError - Provide fallbacks - Plain text when rich isn't installed
- Use colors meaningfully - Red for errors, green for success
- Keep it simple - Don't overdo it
Remember: A CLI should work perfectly without rich - colors are a nice enhancement, not a requirement!
Back to: Episode 2: Entry Points & CLI Tools