python

exercises

exercises.py🐍
"""
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"])
Exercises - Python Tutorial | DeepML