#!/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 shout: bool run_tests: bool def positive_int(value: str) -> int: number = int(value) if number < 1: raise argparse.ArgumentTypeError("must be at least 1") return number def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Print a friendly 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 greetings to print", ) parser.add_argument( "-s", "--shout", action="store_true", help="print the greeting in uppercase", ) parser.add_argument( "--run-tests", 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, count=args.count, shout=args.shout, run_tests=args.run_tests, ) def make_greeting(name: str, shout: bool = False) -> str: greeting = f"Hello, {name}!" return greeting.upper() if shout else greeting def run_cli(options: Options, stdout: TextIO = sys.stdout) -> int: # Keep output logic separate so it is easy to test. for _ in range(options.count): print(make_greeting(options.name, options.shout), file=stdout) return 0 class ParserTests(unittest.TestCase): def test_defaults(self) -> None: self.assertEqual( parse_options([]), Options(name="world", count=1, shout=False, run_tests=False), ) def test_custom_arguments(self) -> None: self.assertEqual( parse_options(["Ada", "--count", "2", "--shout"]), Options(name="Ada", count=2, shout=True, run_tests=False), ) def main(argv: Sequence[str] | None = None) -> int: options = parse_options(argv) if options.run_tests: suite = unittest.defaultTestLoader.loadTestsFromTestCase(ParserTests) result = unittest.TextTestRunner(verbosity=2).run(suite) return 0 if result.wasSuccessful() else 1 return run_cli(options) if __name__ == "__main__": raise SystemExit(main())