#!/usr/bin/env python3 """A tiny, testable CLI argument parser.""" from __future__ import annotations import argparse import sys import unittest from dataclasses import dataclass from typing import Sequence @dataclass(frozen=True) class Options: name: str repeat: int uppercase: bool def positive_int(value: str) -> int: """Parse a positive integer for argparse.""" parsed = int(value) if parsed < 1: raise argparse.ArgumentTypeError("must be at least 1") return parsed def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Print a small greeting.") parser.add_argument("name", help="name to greet") parser.add_argument( "-r", "--repeat", type=positive_int, default=1, help="number of times to print the greeting", ) parser.add_argument( "-u", "--uppercase", action="store_true", help="print the greeting in uppercase", ) parser.add_argument( "--self-test", action="store_true", help="run the built-in tests and exit", ) return parser def parse_options(argv: Sequence[str] | None = None) -> Options: args = build_parser().parse_args(argv) return Options(name=args.name, repeat=args.repeat, uppercase=args.uppercase) def make_greeting(options: Options) -> str: greeting = f"Hello, {options.name}!" if options.uppercase: greeting = greeting.upper() return "\n".join(greeting for _ in range(options.repeat)) def run_tests() -> int: suite = unittest.defaultTestLoader.loadTestsFromTestCase(ParserTests) result = unittest.TextTestRunner(verbosity=2).run(suite) return 0 if result.wasSuccessful() else 1 def main(argv: Sequence[str] | None = None) -> int: argv = sys.argv[1:] if argv is None else argv # Handle tests before normal parsing so no name argument is required. if "--self-test" in argv: return run_tests() options = parse_options(argv) print(make_greeting(options)) return 0 class ParserTests(unittest.TestCase): def test_parse_options_with_defaults(self) -> None: options = parse_options(["Ada"]) self.assertEqual(options, Options(name="Ada", repeat=1, uppercase=False)) def test_make_greeting_repeated_and_uppercase(self) -> None: options = Options(name="Ada", repeat=2, uppercase=True) self.assertEqual(make_greeting(options), "HELLO, ADA!\nHELLO, ADA!") if __name__ == "__main__": raise SystemExit(main())