#!/usr/bin/env python3 from __future__ import annotations import argparse import sys import unittest from dataclasses import dataclass from typing import Sequence, TextIO @dataclass(frozen=True) class Options: name: str count: int uppercase: bool def positive_int(value: str) -> int: """Parse a strictly positive integer for argparse.""" try: parsed = int(value) except ValueError as exc: raise argparse.ArgumentTypeError(f"{value!r} is not an integer") from exc if parsed < 1: raise argparse.ArgumentTypeError("value must be at least 1") return parsed def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Print a configurable greeting.") parser.add_argument("name", nargs="?", default="world", help="name to greet") parser.add_argument( "-c", "--count", 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", ) return parser def parse_options(argv: Sequence[str]) -> Options: args = build_parser().parse_args(argv) return Options(name=args.name, count=args.count, uppercase=args.uppercase) def render_greeting(options: Options) -> str: greeting = f"Hello, {options.name}!" if options.uppercase: greeting = greeting.upper() return "\n".join(greeting for _ in range(options.count)) def main(argv: Sequence[str] | None = None, stdout: TextIO = sys.stdout) -> int: options = parse_options(sys.argv[1:] if argv is None else argv) print(render_greeting(options), file=stdout) return 0 class CliTests(unittest.TestCase): def test_defaults(self) -> None: self.assertEqual(parse_options([]), Options(name="world", count=1, uppercase=False)) def test_render_uppercase_repeated(self) -> None: options = Options(name="Ada", count=2, uppercase=True) self.assertEqual(render_greeting(options), "HELLO, ADA!\nHELLO, ADA!") if __name__ == "__main__": raise SystemExit(main())