python
exercises
exercises.py🐍python
"""
CLI Applications - Exercises
Build command-line tools to practice CLI development.
Run with: pytest 26_cli_applications/exercises.py -v
"""
import argparse
import sys
from pathlib import Path
from typing import Callable, Any
from io import StringIO
# =============================================================================
# EXERCISE 1: Argument Parser for File Converter
# =============================================================================
def create_converter_parser() -> argparse.ArgumentParser:
"""
Create an argument parser for a file converter CLI.
Requirements:
- Positional argument: 'input' - input file path
- Optional argument: '-o/--output' - output file path (default: 'output.txt')
- Optional argument: '-f/--format' - output format, choices: 'json', 'xml', 'csv' (default: 'json')
- Optional argument: '--pretty' - flag for pretty printing
- Optional argument: '-v/--verbose' - flag for verbose output
- Optional argument: '--indent' - indentation level (int, default: 2)
Example usage:
converter input.txt -o output.json -f json --pretty -v --indent 4
"""
# YOUR CODE HERE
pass
def test_converter_parser():
"""Test the converter argument parser."""
parser = create_converter_parser()
# Basic usage
args = parser.parse_args(['input.txt'])
assert args.input == 'input.txt'
assert args.output == 'output.txt'
assert args.format == 'json'
assert args.pretty == False
assert args.verbose == False
assert args.indent == 2
# Full usage
args = parser.parse_args([
'data.csv', '-o', 'result.json', '-f', 'json',
'--pretty', '-v', '--indent', '4'
])
assert args.input == 'data.csv'
assert args.output == 'result.json'
assert args.format == 'json'
assert args.pretty == True
assert args.verbose == True
assert args.indent == 4
# =============================================================================
# EXERCISE 2: Subcommand Parser
# =============================================================================
def create_todo_parser() -> argparse.ArgumentParser:
"""
Create an argument parser for a todo CLI with subcommands.
Subcommands:
1. 'add' - Add a new todo
- Positional: 'text' - todo text
- Optional: '-p/--priority' - choices: 'low', 'medium', 'high' (default: 'medium')
- Optional: '-d/--due' - due date string
2. 'list' - List todos
- Optional: '--all' - flag to show completed todos
- Optional: '--priority' - filter by priority
3. 'done' - Mark todo as done
- Positional: 'id' - todo ID (int)
4. 'delete' - Delete a todo
- Positional: 'id' - todo ID (int)
- Optional: '--force' - skip confirmation
Example:
todo add "Buy groceries" -p high -d "2024-01-20"
todo list --all --priority high
todo done 5
todo delete 3 --force
"""
# YOUR CODE HERE
pass
def test_todo_parser():
"""Test the todo argument parser."""
parser = create_todo_parser()
# Test 'add' command
args = parser.parse_args(['add', 'Buy groceries', '-p', 'high', '-d', '2024-01-20'])
assert args.command == 'add'
assert args.text == 'Buy groceries'
assert args.priority == 'high'
assert args.due == '2024-01-20'
# Test 'list' command
args = parser.parse_args(['list', '--all', '--priority', 'high'])
assert args.command == 'list'
assert args.all == True
assert args.priority == 'high'
# Test 'done' command
args = parser.parse_args(['done', '5'])
assert args.command == 'done'
assert args.id == 5
# Test 'delete' command
args = parser.parse_args(['delete', '3', '--force'])
assert args.command == 'delete'
assert args.id == 3
assert args.force == True
# =============================================================================
# EXERCISE 3: Simple CLI Framework
# =============================================================================
class MiniCLI:
"""
Implement a minimal CLI framework.
Requirements:
- register(name, handler, help_text) - register a command
- run(args) - parse and execute command
- help() - print help for all commands
Handler functions receive a list of string arguments.
Example:
cli = MiniCLI("myapp")
def greet(args):
name = args[0] if args else "World"
return f"Hello, {name}!"
cli.register("greet", greet, "Greet someone")
result = cli.run(["greet", "Alice"])
# Returns: "Hello, Alice!"
"""
def __init__(self, name: str):
# YOUR CODE HERE
pass
def register(self, name: str, handler: Callable[[list[str]], Any], help_text: str = ""):
# YOUR CODE HERE
pass
def run(self, args: list[str]) -> Any:
"""
Run a command with given arguments.
Returns the result of the handler.
Returns help text if no command given or command is 'help'.
Raises ValueError for unknown commands.
"""
# YOUR CODE HERE
pass
def help(self) -> str:
"""Return help text for all commands."""
# YOUR CODE HERE
pass
def test_mini_cli():
"""Test the MiniCLI framework."""
cli = MiniCLI("testapp")
def greet(args):
name = args[0] if args else "World"
return f"Hello, {name}!"
def add(args):
if len(args) < 2:
return "Error: need two numbers"
return int(args[0]) + int(args[1])
cli.register("greet", greet, "Greet someone by name")
cli.register("add", add, "Add two numbers")
# Test commands
assert cli.run(["greet", "Alice"]) == "Hello, Alice!"
assert cli.run(["greet"]) == "Hello, World!"
assert cli.run(["add", "5", "3"]) == 8
# Test help
help_text = cli.help()
assert "greet" in help_text
assert "add" in help_text
# Test unknown command
try:
cli.run(["unknown"])
assert False, "Should raise ValueError"
except ValueError:
pass
# =============================================================================
# EXERCISE 4: Progress Tracker
# =============================================================================
class ProgressTracker:
"""
Implement a progress tracker for CLI output.
Requirements:
- __init__(total, width, prefix, suffix)
- update(current) - update progress and return progress string
- increment() - increment by 1 and return progress string
- complete() - mark as complete and return final string
Progress string format:
"Processing |████████░░░░░░░░░░░░| 40% (40/100)"
Example:
tracker = ProgressTracker(100, width=20, prefix="Downloading")
print(tracker.update(50)) # "Downloading |██████████░░░░░░░░░░| 50% (50/100)"
"""
def __init__(
self,
total: int,
width: int = 20,
prefix: str = "Progress",
fill: str = "█",
empty: str = "░"
):
# YOUR CODE HERE
pass
def update(self, current: int) -> str:
# YOUR CODE HERE
pass
def increment(self) -> str:
# YOUR CODE HERE
pass
def complete(self) -> str:
# YOUR CODE HERE
pass
def test_progress_tracker():
"""Test the ProgressTracker."""
tracker = ProgressTracker(100, width=10, prefix="Test")
# Test update
result = tracker.update(50)
assert "50%" in result
assert "Test" in result
# Test progress bar visualization
assert "█" in result
assert "░" in result
# Test increment
result = tracker.increment()
assert "51%" in result
# Test complete
result = tracker.complete()
assert "100%" in result
# =============================================================================
# EXERCISE 5: Configuration Loader
# =============================================================================
import json
import os
class ConfigLoader:
"""
Implement a configuration loader with multiple sources.
Priority (highest to lowest):
1. Command-line arguments
2. Environment variables (with prefix)
3. Config file
4. Default values
Example:
loader = ConfigLoader(prefix="MYAPP_")
loader.set_defaults({"debug": False, "port": 8000})
loader.load_file("config.json") # {"port": 3000}
# Environment: MYAPP_DEBUG=true
# Args: ["--port", "5000"]
config = loader.get_config(["--port", "5000"])
# Result: {"debug": True, "port": 5000}
"""
def __init__(self, prefix: str = ""):
# YOUR CODE HERE
pass
def set_defaults(self, defaults: dict) -> 'ConfigLoader':
"""Set default configuration values."""
# YOUR CODE HERE
pass
def load_file(self, path: str) -> 'ConfigLoader':
"""Load configuration from JSON file."""
# YOUR CODE HERE
pass
def load_env(self) -> 'ConfigLoader':
"""Load configuration from environment variables."""
# YOUR CODE HERE
pass
def parse_args(self, args: list[str]) -> dict:
"""
Parse command-line arguments.
Format: --key value or --flag (for boolean true)
"""
# YOUR CODE HERE
pass
def get_config(self, args: list[str] | None = None) -> dict:
"""Get merged configuration from all sources."""
# YOUR CODE HERE
pass
def test_config_loader(tmp_path):
"""Test the ConfigLoader."""
# Create temp config file
config_file = tmp_path / "config.json"
config_file.write_text('{"port": 3000, "host": "localhost"}')
# Set environment variable
os.environ["TEST_DEBUG"] = "true"
os.environ["TEST_WORKERS"] = "4"
try:
loader = ConfigLoader(prefix="TEST_")
loader.set_defaults({"debug": False, "port": 8000, "workers": 1})
loader.load_file(str(config_file))
loader.load_env()
config = loader.get_config(["--port", "5000"])
# Args override file
assert config["port"] == 5000
# File value preserved
assert config["host"] == "localhost"
# Env overrides default
assert config["debug"] == True or config["debug"] == "true"
assert config["workers"] == 4 or config["workers"] == "4"
finally:
del os.environ["TEST_DEBUG"]
del os.environ["TEST_WORKERS"]
# =============================================================================
# EXERCISE 6: Command Validator
# =============================================================================
from dataclasses import dataclass
@dataclass
class ValidationError:
"""Validation error with field and message."""
field: str
message: str
class CommandValidator:
"""
Implement a command-line argument validator.
Requirements:
- add_rule(field, validator, message) - add validation rule
- validate(args_dict) - validate and return list of errors
Validator is a callable that takes a value and returns bool.
Example:
validator = CommandValidator()
validator.add_rule("port", lambda x: 1 <= x <= 65535, "Port must be 1-65535")
validator.add_rule("host", lambda x: len(x) > 0, "Host is required")
errors = validator.validate({"port": 70000, "host": ""})
# Returns: [ValidationError("port", "..."), ValidationError("host", "...")]
"""
def __init__(self):
# YOUR CODE HERE
pass
def add_rule(self, field: str, validator: Callable[[Any], bool], message: str):
# YOUR CODE HERE
pass
def add_required(self, field: str, message: str | None = None):
"""Add a required field rule."""
# YOUR CODE HERE
pass
def add_type(self, field: str, expected_type: type, message: str | None = None):
"""Add a type validation rule."""
# YOUR CODE HERE
pass
def validate(self, args: dict) -> list[ValidationError]:
# YOUR CODE HERE
pass
def is_valid(self, args: dict) -> bool:
"""Return True if all validations pass."""
# YOUR CODE HERE
pass
def test_command_validator():
"""Test the CommandValidator."""
validator = CommandValidator()
validator.add_rule("port", lambda x: 1 <= x <= 65535, "Port must be 1-65535")
validator.add_required("host")
validator.add_type("workers", int, "Workers must be an integer")
validator.add_rule("workers", lambda x: x > 0, "Workers must be positive")
# Valid input
errors = validator.validate({
"port": 8080,
"host": "localhost",
"workers": 4
})
assert len(errors) == 0
# Invalid input
errors = validator.validate({
"port": 70000,
"host": "",
"workers": "four"
})
assert len(errors) >= 2
# Check is_valid
assert validator.is_valid({"port": 8080, "host": "localhost", "workers": 4}) == True
assert validator.is_valid({"port": 70000, "host": ""}) == False
# =============================================================================
# EXERCISE 7: Interactive Menu
# =============================================================================
class InteractiveMenu:
"""
Implement an interactive menu system.
Requirements:
- add_item(key, label, handler) - add menu item
- add_separator() - add visual separator
- display() - return menu display string
- handle(key) - execute handler for key, return result
Example:
menu = InteractiveMenu("Main Menu")
menu.add_item("1", "View items", view_items)
menu.add_item("2", "Add item", add_item)
menu.add_separator()
menu.add_item("q", "Quit", quit_app)
print(menu.display())
# Main Menu
# ─────────
# [1] View items
# [2] Add item
# ─────────────
# [q] Quit
result = menu.handle("1") # Calls view_items()
"""
def __init__(self, title: str):
# YOUR CODE HERE
pass
def add_item(self, key: str, label: str, handler: Callable[[], Any]):
# YOUR CODE HERE
pass
def add_separator(self):
# YOUR CODE HERE
pass
def display(self) -> str:
# YOUR CODE HERE
pass
def handle(self, key: str) -> Any:
"""
Execute handler for given key.
Return handler result or None if key not found.
"""
# YOUR CODE HERE
pass
def get_keys(self) -> list[str]:
"""Return list of valid keys."""
# YOUR CODE HERE
pass
def test_interactive_menu():
"""Test the InteractiveMenu."""
results = []
menu = InteractiveMenu("Test Menu")
menu.add_item("1", "Option One", lambda: results.append("one"))
menu.add_item("2", "Option Two", lambda: results.append("two"))
menu.add_separator()
menu.add_item("q", "Quit", lambda: results.append("quit"))
# Test display
display = menu.display()
assert "Test Menu" in display
assert "[1]" in display
assert "Option One" in display
assert "[q]" in display
# Test handler
menu.handle("1")
assert "one" in results
menu.handle("2")
assert "two" in results
# Test keys
keys = menu.get_keys()
assert "1" in keys
assert "2" in keys
assert "q" in keys
# =============================================================================
# RUN TESTS
# =============================================================================
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])