From e73218d9f4aea4237cbf58e54483d1f91ef7a9ba Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Sun, 4 Oct 2026 14:13:01 +0800 Subject: [PATCH] feat: implement CLI program with user prompts and tests - Add main.py with input prompts, bid validation, screen clearing, winner announcement - Add tests/test_main.py with monkeypatched input tests - Update docs/plan.md to mark Milestone 3 as complete --- docs/plan.md | 80 +++++++++++++++++++------------------- src/secret_auction/main.py | 60 ++++++++++++++++++++++++++++ tests/test_main.py | 35 +++++++++++++++++ 3 files changed, 135 insertions(+), 40 deletions(-) create mode 100644 src/secret_auction/main.py create mode 100644 tests/test_main.py diff --git a/docs/plan.md b/docs/plan.md index 8b682b7..db193ee 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -1,40 +1,40 @@ -# Plan: Secret Auction Program (Day 9) - -## Context - -Udemy 100 Days of Code blind-auction CLI. The repository uses a Python `.gitignore`, an AGPL `LICENSE` and a git-ignored `.env` (tokens, never printed or committed). Python 3.13 or newer is required. - -## Working method - -Work on ONE milestone at a time. Each milestone is one branch (`milestone/NN-name`) and one pull request. When a milestone is finished, stop, verify it and review before starting the next. Milestones and issues are also tracked on the Gitea repository (via API, token from `.env`). - -## Milestone 1 - Project scaffold and repo metadata (`milestone/01-scaffold`) - -- [x] Create `src/secret_auction/__init__.py`, `tests/`, `docs/` -- [x] `pyproject.toml`: `requires-python >=3.13`, src layout, no runtime deps, `dev` extra (`pytest`), pytest config, console script -- [x] `.env.example`; `.env` and `.venv/` ignored; add `docs/doxygen/` to `.gitignore` -- [x] `constants.py` -- [x] `Doxyfile` -- [x] Set repository description and topics (`python`, `udemy`, `100-days-of-code`, `blind-auction`, `beginner`) via API -- [x] Create milestones and issues on the host (milestones M1-M4, issues #1-#12) -- [x] Verify: create `.venv`, upgrade pip, `pip install -e ".[dev]"` -- [x] Save plan and milestones in `docs/plan.md` - -## Milestone 2 - Core auction logic (`milestone/02-core-logic`) - -- [x] `auction.py` (Doxygen comments): `add_bid`, `find_winner` (uses `max(bids, key=bids.get)`, `ValueError` on empty) -- [x] `tests/test_auction.py` -- Verify: `pytest` passes - -## Milestone 3 - CLI program (`milestone/03-cli`) - -- [ ] `main.py`: prompt name and numeric bid (re-prompt on invalid), bids dict kept outside the loop, "other bidders? yes/no", clear screen with newlines between bidders, announce the winner -- [ ] `tests/test_main.py` with monkeypatched `input` -- Verify: `pytest`; scripted run `python -m secret_auction.main` with two bidders - -## Milestone 4 - Documentation (`milestone/04-docs`) - -- [ ] `README.md` from the template (Overview, Requirements, Setup incl. creating/activating `.venv` and `python -m pip install --upgrade pip`, Run, Tests, License AGPL, Links) -- [ ] `docs/flowchart.md` (Mermaid plus link to the diagrams.net chart) -- [ ] Generate Doxygen docs if `doxygen` is installed -- Verify: README commands work verbatim; `.env` stays untracked +# Plan: Secret Auction Program (Day 9) + +## Context + +Udemy 100 Days of Code blind-auction CLI. The repository uses a Python `.gitignore`, an AGPL `LICENSE` and a git-ignored `.env` (tokens, never printed or committed). Python 3.13 or newer is required. + +## Working method + +Work on ONE milestone at a time. Each milestone is one branch (`milestone/NN-name`) and one pull request. When a milestone is finished, stop, verify it and review before starting the next. Milestones and issues are also tracked on the Gitea repository (via API, token from `.env`). + +## Milestone 1 - Project scaffold and repo metadata (`milestone/01-scaffold`) + +- [x] Create `src/secret_auction/__init__.py`, `tests/`, `docs/` +- [x] `pyproject.toml`: `requires-python >=3.13`, src layout, no runtime deps, `dev` extra (`pytest`), pytest config, console script +- [x] `.env.example`; `.env` and `.venv/` ignored; add `docs/doxygen/` to `.gitignore` +- [x] `constants.py` +- [x] `Doxyfile` +- [x] Set repository description and topics (`python`, `udemy`, `100-days-of-code`, `blind-auction`, `beginner`) via API +- [x] Create milestones and issues on the host (milestones M1-M4, issues #1-#12) +- [x] Verify: create `.venv`, upgrade pip, `pip install -e ".[dev]"` +- [x] Save plan and milestones in `docs/plan.md` + +## Milestone 2 - Core auction logic (`milestone/02-core-logic`) + +- [x] `auction.py` (Doxygen comments): `add_bid`, `find_winner` (uses `max(bids, key=bids.get)`, `ValueError` on empty) +- [x] `tests/test_auction.py` +- Verify: `pytest` passes + +## Milestone 3 - CLI program (`milestone/03-cli`) + +- [x] `main.py`: prompt name and numeric bid (re-prompt on invalid), bids dict kept outside the loop, "other bidders? yes/no", clear screen with newlines between bidders, announce the winner +- [x] `tests/test_main.py` with monkeypatched `input` +- Verify: `pytest`; scripted run `python -m secret_auction.main` with two bidders + +## Milestone 4 - Documentation (`milestone/04-docs`) + +- [ ] `README.md` from the template (Overview, Requirements, Setup incl. creating/activating `.venv` and `python -m pip install --upgrade pip`, Run, Tests, License AGPL, Links) +- [ ] `docs/flowchart.md` (Mermaid plus link to the diagrams.net chart) +- [ ] Generate Doxygen docs if `doxygen` is installed +- Verify: README commands work verbatim; `.env` stays untracked diff --git a/src/secret_auction/main.py b/src/secret_auction/main.py new file mode 100644 index 0000000..eb8be52 --- /dev/null +++ b/src/secret_auction/main.py @@ -0,0 +1,60 @@ +"""! @file main.py +@brief Command line interface for the secret auction program. +""" + +import math + +from secret_auction import constants +from secret_auction.auction import add_bid, find_winner + + +def clear_screen() -> None: + """! @brief Hide previous entries by printing blank lines.""" + print("\n" * constants.CLEAR_SCREEN_LINES) + + +def ask_bid() -> float: + """! @brief Ask for a bid until a valid, non-negative number is entered. + @return The bid amount. + """ + while True: + try: + amount = float(input(constants.PROMPT_BID)) + except ValueError: + print(constants.MSG_INVALID_BID) + continue + if math.isfinite(amount) and amount >= 0: + return amount + print(constants.MSG_INVALID_BID) + + +def ask_more_bidders() -> bool: + """! @brief Ask whether there are more bidders until yes/no is answered. + @return True if more bidders exist, False otherwise. + """ + while True: + answer = input(constants.PROMPT_MORE_BIDDERS).strip().lower() + if answer in constants.YES_ANSWERS: + return True + if answer in constants.NO_ANSWERS: + return False + print(constants.MSG_INVALID_ANSWER) + + +def main() -> None: + """! @brief Run the blind auction: collect bids, then announce the winner.""" + print(constants.MSG_WELCOME) + bids: dict[str, float] = {} # kept outside the loop so bids are not lost + more_bidders = True + while more_bidders: + name = input(constants.PROMPT_NAME).strip() + add_bid(bids, name, ask_bid()) + more_bidders = ask_more_bidders() + if more_bidders: + clear_screen() + winner, amount = find_winner(bids) + print(constants.MSG_WINNER.format(name=winner, amount=amount)) + + +if __name__ == "__main__": + main() diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..385be0d --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,35 @@ +from secret_auction import constants +from secret_auction.main import ask_bid, ask_more_bidders, main + + +def feed(monkeypatch, answers): + it = iter(answers) + monkeypatch.setattr("builtins.input", lambda _prompt="": next(it)) + + +def test_ask_bid_reprompts_on_invalid(monkeypatch, capsys): + feed(monkeypatch, ["abc", "-5", "nan", "12.5"]) + assert ask_bid() == 12.5 + assert capsys.readouterr().out.count(constants.MSG_INVALID_BID) == 3 + + +def test_ask_more_bidders(monkeypatch, capsys): + feed(monkeypatch, ["maybe", "Y"]) + assert ask_more_bidders() is True + assert constants.MSG_INVALID_ANSWER in capsys.readouterr().out + feed(monkeypatch, ["no"]) + assert ask_more_bidders() is False + + +def test_main_announces_highest_bidder_and_clears_screen(monkeypatch, capsys): + feed(monkeypatch, ["Ann", "10", "yes", "Bob", "25.5", "yes", "Cy", "3", "no"]) + main() + out = capsys.readouterr().out + assert "The winner is Bob with a bid of $25.50." in out + assert out.count("\n" * constants.CLEAR_SCREEN_LINES) == 2 + + +def test_main_single_bidder(monkeypatch, capsys): + feed(monkeypatch, ["Ann", "7", "n"]) + main() + assert "The winner is Ann with a bid of $7.00." in capsys.readouterr().out