From d7c45320d545b47d45d65174544101cdbe11f641 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Fri, 6 Nov 2026 13:35:21 +0800 Subject: [PATCH 1/3] Add speed calc decorator, tests and lecture-step screenshots Package src/speed_calc with the decorator, fast_function, slow_function, constants.py and runnable lecture steps; pytest tests with a fake clock; four screenshots rendered from real output; README lecture steps; review record RC-007. Task: MIL-002#1 Task: MIL-002#2 Task: MIL-002#3 Task: MIL-002#4 Task: MIL-002#5 Task: MIL-002#6 Task: MIL-002#7 --- README.md | 308 ++++++++++-------- docs/artifact-registry.md | 2 +- docs/screenshots/generate_screenshots.py | 86 +++++ docs/screenshots/step-1-current-time.png | Bin 0 -> 6239 bytes .../screenshots/step-2-decorator-skeleton.png | Bin 0 -> 10217 bytes docs/screenshots/step-3-timing-wrapper.png | Bin 0 -> 9350 bytes docs/screenshots/step-4-fast-vs-slow.png | Bin 0 -> 15937 bytes .../reviews/rc-007-python-source-mil-002.md | 73 +++++ docs/sqa/traceability-matrix.md | 178 +++++----- pyproject.toml | 9 +- src/speed_calc/__init__.py | 14 + src/speed_calc/__main__.py | 21 ++ src/speed_calc/constants.py | 13 + src/speed_calc/decorator.py | 51 +++ src/speed_calc/demo_functions.py | 25 ++ src/speed_calc/lecture_steps.py | 78 +++++ tests/test_decorator.py | 60 ++++ tests/test_demo_functions.py | 33 ++ tests/test_lecture_steps.py | 36 ++ 19 files changed, 763 insertions(+), 224 deletions(-) create mode 100644 docs/screenshots/generate_screenshots.py create mode 100644 docs/screenshots/step-1-current-time.png create mode 100644 docs/screenshots/step-2-decorator-skeleton.png create mode 100644 docs/screenshots/step-3-timing-wrapper.png create mode 100644 docs/screenshots/step-4-fast-vs-slow.png create mode 100644 docs/sqa/reviews/rc-007-python-source-mil-002.md create mode 100644 src/speed_calc/__init__.py create mode 100644 src/speed_calc/__main__.py create mode 100644 src/speed_calc/constants.py create mode 100644 src/speed_calc/decorator.py create mode 100644 src/speed_calc/demo_functions.py create mode 100644 src/speed_calc/lecture_steps.py create mode 100644 tests/test_decorator.py create mode 100644 tests/test_demo_functions.py create mode 100644 tests/test_lecture_steps.py diff --git a/README.md b/README.md index 09c1881..8821ecb 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,174 @@ -# โฑ๏ธ Speed Calc Decorator - -A Python decorator that measures how many seconds a function takes to run. It is -my solution to a lecture exercise from Udemy's *100 Days of Code: The Complete -Python Pro Bootcamp*, written for fellow course participants and for anyone -browsing for ideas. It has no runtime dependencies. - -## ๐Ÿ“š Table of Contents - -- [Overview](#-overview) -- [Requirements](#-requirements) -- [Setup](#-setup) -- [Run](#-run) -- [Tests](#-tests) -- [Documentation](#-documentation) -- [License](#-license) -- [Links](#-links) -- [Appendix: Project documents](#-appendix-project-documents) - -## ๐Ÿงญ Overview - -The assignment: write `speed_calc_decorator` and use it to print how long -`fast_function` and `slow_function` take. Expected output has this shape: - -```text -1695050908.1985211 -fast_function run speed: 0.33974480628967285s -slow_function run speed: 2.9590742588043213s -``` - -The numbers differ on every machine. The repository is laid out as: - -| Folder | Purpose | -| --- | --- | -| `src/speed_calc/` | The package: the decorator, the two demo functions, `constants.py` | -| `tests/` | pytest tests; they use a fake clock, so they never depend on real time | -| `docs/` | Project documents (Business Case, plan, milestones), screenshots and the wiki | - -## ๐Ÿ“‹ Requirements - -- Python 3.13 or newer (`python --version`). -- `git`, to clone the repository. -- Optional: [Doxygen](https://www.doxygen.nl/) to build the API documentation. - -There are no runtime dependencies. `pytest` is needed only to run the tests. - -## ๐Ÿ› ๏ธ Setup - -Create a local virtual environment named `.venv` in the project folder and -activate it. Windows (PowerShell): - -```powershell -python -m venv .venv -.venv\Scripts\Activate.ps1 -``` - -Linux and macOS: - -```bash -python3 -m venv .venv -source .venv/bin/activate -``` - -Upgrade pip, then install the project with the test tools: - -```bash -python -m pip install --upgrade pip -python -m pip install -e ".[dev]" -``` - -Leave the environment with `deactivate`. Delete the `.venv` folder to start over. -The `.venv` folder is ignored by git. - -## โ–ถ๏ธ Run - -With the environment active: - -```bash -python -m speed_calc -``` - -## ๐Ÿงช Tests - -```bash -python -m pytest -``` - -## ๐Ÿ“– Documentation - -The source uses Doxygen comments. Build the HTML documentation into -`docs/doxygen/`: - -```bash -doxygen Doxyfile -``` - -## ๐Ÿ“„ License - -GNU Affero General Public License v3.0, see [LICENSE](./LICENSE). - -## ๐Ÿ”— Links - -- [Repository][repository] -- [Wiki][documentation] -- [Issue tracker][issues] - -## ๐Ÿ“Ž Appendix: Project documents - -| Document | What it tells you | -| --- | --- | -| [AGENTS.md][agents] | The rules AI agents follow in this repository | -| [Business Case][BC-001] | Why the project exists, its scope and success criteria | -| [Stakeholder Analysis][SA-001] | Who is involved; the stakeholder IDs used for owners and reviewers | -| [Project Plan][PP-001] | The gateways and their schedule | -| [Milestones][milestones] | One document per gateway: Go/No-Go criteria and tasks | -| [Artifact registry][registry] | Where each artifact type lives, the next version, the PO language and domain | -| [Traceability matrix][TM-001] | How the artifacts link and when each was last reviewed | -| [Framework README][framework-readme] | The framework this project uses | -| [Framework changelog][framework-changelog] | What changed in each framework release | - ---- - -[repository]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/054-speed-calc-decorator -[documentation]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/054-speed-calc-decorator/wiki -[issues]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/054-speed-calc-decorator/issues -[agents]: ./AGENTS.md -[BC-001]: ./docs/business-case.md -[SA-001]: ./docs/stakeholder-analysis.md -[PP-001]: ./docs/project-plan.md -[milestones]: ./docs/milestones/ -[registry]: ./docs/artifact-registry.md -[TM-001]: ./docs/sqa/traceability-matrix.md -[framework-readme]: ./framework/README.md -[framework-changelog]: ./framework/CHANGELOG.md +# โฑ๏ธ Speed Calc Decorator + +A Python decorator that measures how many seconds a function takes to run. It is +my solution to a lecture exercise from Udemy's *100 Days of Code: The Complete +Python Pro Bootcamp*, written for fellow course participants and for anyone +browsing for ideas. It has no runtime dependencies. + +## ๐Ÿ“š Table of Contents + +- [Overview](#-overview) +- [Requirements](#-requirements) +- [Setup](#-setup) +- [Run](#-run) +- [Lecture steps](#-lecture-steps) +- [Tests](#-tests) +- [Documentation](#-documentation) +- [License](#-license) +- [Links](#-links) +- [Appendix: Project documents](#-appendix-project-documents) + +## ๐Ÿงญ Overview + +The assignment: write `speed_calc_decorator` and use it to print how long +`fast_function` and `slow_function` take. Expected output has this shape: + +```text +1695050908.1985211 +fast_function run speed: 0.33974480628967285s +slow_function run speed: 2.9590742588043213s +``` + +The numbers differ on every machine. The repository is laid out as: + +| Folder | Purpose | +| --- | --- | +| `src/speed_calc/` | The package: the decorator, the two demo functions, `constants.py` | +| `tests/` | pytest tests; they use a fake clock, so they never depend on real time | +| `docs/` | Project documents (Business Case, plan, milestones), screenshots and the wiki | + +## ๐Ÿ“‹ Requirements + +- Python 3.13 or newer (`python --version`). +- `git`, to clone the repository. +- Optional: [Doxygen](https://www.doxygen.nl/) to build the API documentation. + +There are no runtime dependencies. `pytest` is needed only to run the tests. + +## ๐Ÿ› ๏ธ Setup + +Create a local virtual environment named `.venv` in the project folder and +activate it. Windows (PowerShell): + +```powershell +python -m venv .venv +.venv\Scripts\Activate.ps1 +``` + +Linux and macOS: + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +Upgrade pip, then install the project with the test tools: + +```bash +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +Leave the environment with `deactivate`. Delete the `.venv` folder to start over. +The `.venv` folder is ignored by git. + +## โ–ถ๏ธ Run + +With the environment active: + +```bash +python -m speed_calc +``` + +It prints the current time, then the run time of `fast_function` and +`slow_function`. The values depend on your machine. + +## ๐Ÿชœ Lecture steps + +Each step runs on its own with `python -m speed_calc.lecture_steps N` (N is 1 to +4); step 4 is the same as `python -m speed_calc`. The screenshots are rendered +from the real output of each command by +[`docs/screenshots/generate_screenshots.py`](./docs/screenshots/generate_screenshots.py). + +### Step 1: the current time + +`time.time()` returns the seconds since January 1st, 1970. + +![Step 1: the current time](./docs/screenshots/step-1-current-time.png) + +### Step 2: the decorator skeleton + +A decorator takes a function and returns a wrapper that calls it. `functools.wraps` +keeps the original name. + +![Step 2: the decorator skeleton](./docs/screenshots/step-2-decorator-skeleton.png) + +### Step 3: timing the call + +The wrapper reads the clock before and after the call and prints the difference. + +![Step 3: the timing wrapper](./docs/screenshots/step-3-timing-wrapper.png) + +### Step 4: fast against slow + +`@speed_calc_decorator` above `fast_function` and `slow_function` gives the +expected output. + +![Step 4: fast against slow](./docs/screenshots/step-4-fast-vs-slow.png) + +## ๐Ÿงช Tests + +```bash +python -m pytest +``` + +All tests pass; they use a fake clock, so they do not depend on how fast your +machine is. + +## ๐Ÿ“– Documentation + +The source uses Doxygen comments. Build the HTML documentation into +`docs/doxygen/`: + +```bash +doxygen Doxyfile +``` + +## ๐Ÿ“„ License + +GNU Affero General Public License v3.0, see [LICENSE](./LICENSE). + +## ๐Ÿ”— Links + +- [Repository][repository] +- [Wiki][documentation] +- [Issue tracker][issues] + +## ๐Ÿ“Ž Appendix: Project documents + +| Document | What it tells you | +| --- | --- | +| [AGENTS.md][agents] | The rules AI agents follow in this repository | +| [Business Case][BC-001] | Why the project exists, its scope and success criteria | +| [Stakeholder Analysis][SA-001] | Who is involved; the stakeholder IDs used for owners and reviewers | +| [Project Plan][PP-001] | The gateways and their schedule | +| [Milestones][milestones] | One document per gateway: Go/No-Go criteria and tasks | +| [Artifact registry][registry] | Where each artifact type lives, the next version, the PO language and domain | +| [Traceability matrix][TM-001] | How the artifacts link and when each was last reviewed | +| [Framework README][framework-readme] | The framework this project uses | +| [Framework changelog][framework-changelog] | What changed in each framework release | + +--- + +[repository]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/054-speed-calc-decorator +[documentation]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/054-speed-calc-decorator/wiki +[issues]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/054-speed-calc-decorator/issues +[agents]: ./AGENTS.md +[BC-001]: ./docs/business-case.md +[SA-001]: ./docs/stakeholder-analysis.md +[PP-001]: ./docs/project-plan.md +[milestones]: ./docs/milestones/ +[registry]: ./docs/artifact-registry.md +[TM-001]: ./docs/sqa/traceability-matrix.md +[framework-readme]: ./framework/README.md +[framework-changelog]: ./framework/CHANGELOG.md diff --git a/docs/artifact-registry.md b/docs/artifact-registry.md index 5a87d59..169546f 100644 --- a/docs/artifact-registry.md +++ b/docs/artifact-registry.md @@ -16,7 +16,7 @@ document of a type. `Primary File` may contain a glob (e.g. | PP | Project Plan | docs/project-plan.md | 002 | | MIL | Milestone / Gateway | docs/milestones/mil-*.md | 004 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | -| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 007 | +| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 008 | | ADR | Architecture Decision Record | docs/adr/adr-*.md | 0002 | ## Languages diff --git a/docs/screenshots/generate_screenshots.py b/docs/screenshots/generate_screenshots.py new file mode 100644 index 0000000..1cb6972 --- /dev/null +++ b/docs/screenshots/generate_screenshots.py @@ -0,0 +1,86 @@ +"""Render the real output of each lecture step to a PNG file. + +Dev-only tool (needs Pillow, installed by `pip install -e ".[dev]"`). It runs +each command, captures its output and draws it as a terminal image, so no +screen is captured and the desktop is never touched. + +Usage, from the project root: python docs/screenshots/generate_screenshots.py +""" + +import subprocess +import sys +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +OUTPUT_DIR = Path(__file__).resolve().parent +ROOT = OUTPUT_DIR.parent.parent +PYTHON = "python" + +# (file name, command shown in the prompt, arguments run after the interpreter) +SHOTS = [ + ( + "step-1-current-time.png", + "python -m speed_calc.lecture_steps 1", + ["-m", "speed_calc.lecture_steps", "1"], + ), + ( + "step-2-decorator-skeleton.png", + "python -m speed_calc.lecture_steps 2", + ["-m", "speed_calc.lecture_steps", "2"], + ), + ( + "step-3-timing-wrapper.png", + "python -m speed_calc.lecture_steps 3", + ["-m", "speed_calc.lecture_steps", "3"], + ), + ("step-4-fast-vs-slow.png", "python -m speed_calc", ["-m", "speed_calc"]), +] + +BACKGROUND, FOREGROUND, PROMPT = "#1e1e2e", "#cdd6f4", "#a6e3a1" +FONT_CANDIDATES = [ + "C:/Windows/Fonts/consola.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + "/System/Library/Fonts/Menlo.ttc", +] + + +def load_font(size: int) -> ImageFont.ImageFont: + for candidate in FONT_CANDIDATES: + if Path(candidate).exists(): + return ImageFont.truetype(candidate, size) + return ImageFont.load_default() + + +def run(arguments: list[str]) -> str: + done = subprocess.run( + [sys.executable, *arguments], + cwd=ROOT, + capture_output=True, + text=True, + check=True, + ) + return done.stdout.strip() + + +def render(path: Path, command: str, output: str) -> None: + font = load_font(20) + lines = [f"$ {command}", *output.splitlines()] + width = int(max(font.getlength(line) for line in lines)) + 48 + height = 28 * len(lines) + 48 + image = Image.new("RGB", (max(width, 640), height), BACKGROUND) + draw = ImageDraw.Draw(image) + for index, line in enumerate(lines): + colour = PROMPT if index == 0 else FOREGROUND + draw.text((24, 24 + 28 * index), line, font=font, fill=colour) + image.save(path) + + +def main() -> None: + for name, command, arguments in SHOTS: + render(OUTPUT_DIR / name, command, run(arguments)) + print("wrote", name) + + +if __name__ == "__main__": + main() diff --git a/docs/screenshots/step-1-current-time.png b/docs/screenshots/step-1-current-time.png new file mode 100644 index 0000000000000000000000000000000000000000..c8181284a2d3abd49313fdcc4d85b2c5a4564890 GIT binary patch literal 6239 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 TggG|AZb7dUH597k-h})I>C^ww literal 0 HcmV?d00001 diff --git a/docs/screenshots/step-2-decorator-skeleton.png b/docs/screenshots/step-2-decorator-skeleton.png new file mode 100644 index 0000000000000000000000000000000000000000..a451818b18db0b0d010245ad700eaf7ff15577ef GIT binary patch literal 10217 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 E3&5)3bN~PV literal 0 HcmV?d00001 diff --git a/docs/screenshots/step-4-fast-vs-slow.png b/docs/screenshots/step-4-fast-vs-slow.png new file mode 100644 index 0000000000000000000000000000000000000000..5326d11347003fd7df700a0af0479212921df62e GIT binary patch literal 15937 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 DXPbp9 literal 0 HcmV?d00001 diff --git a/docs/sqa/reviews/rc-007-python-source-mil-002.md b/docs/sqa/reviews/rc-007-python-source-mil-002.md new file mode 100644 index 0000000..3918c02 --- /dev/null +++ b/docs/sqa/reviews/rc-007-python-source-mil-002.md @@ -0,0 +1,73 @@ +# RC-007 Review of the Python source and tests of MIL-002 + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-007 | +| CrossReference | [QC-PY-001], [MIL-002] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-11-06 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: the Python code under `src/speed_calc/` and `tests/`, plus the dev tool `docs/screenshots/generate_screenshots.py`, delivered by [MIL-002] (source code has no instance ID) +- Checklist used: [QC-PY-001] +- Scope: full review. Round 1 found one defect (criterion 4, unannotated tests), fixed in the tests; round 2 re-checked every criterion and is shown here. +- Language and domain: n/a (technical type) +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`; S01 is also the author) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing | Pass | `ruff` rule set N (pep8-naming) passes; modules snake_case, `FAST_ITERATIONS` UPPER_SNAKE | +| 2 | Names state purpose in the domain's language | Pass | `speed_calc_decorator`, `make_speed_calc_decorator`, `RESULT_TEMPLATE`; loop variable `i` is a tiny scope | +| 3 | Formatter and linter, no unexplained suppressions | Pass | `ruff check src tests docs/screenshots`: All checks passed; `ruff format --check`: 10 files formatted. One `# type: ignore[attr-defined]` in `lecture_steps.py`, with the reason in the comment above it | +| 4 | Every signature type-annotated, including `-> None` | Pass | Round 1 Fail: tests had no annotations. Fixed in round 2 (`-> None`, `pytest.CaptureFixture[str]`, `make_clock` return type); `mypy --strict tests src`: no issues in 9 files | +| 5 | No bare `except:`, no swallowed exceptions | Pass | No `try`/`except` in the code | +| 6 | No mutable defaults, no shadowed builtins | Pass | Defaults are `time.time` and `print`; ruff B passes; no builtin is shadowed | +| 7 | Context managers for files, locks, connections | N-A | No resources are opened (the screenshot tool uses `subprocess.run` and `Image.save`, which need none) | +| 8 | Docstrings that say what, not how | Pass | Every module, function and constant has a Doxygen comment (`@brief`, `@param`, `@return`) | +| 9 | Logging uses `logging`, not `print`; no secrets | Pass | `print` is the assignment's required program output (`python -m speed_calc` prints the time and timing lines), not diagnostics; the decorator takes an injectable `output`. No secrets or personal data are printed | +| 10 | Classes and operations trace to the DCD | N-A | No DCD exists by decision ([ADR-0001]); the four functions trace to MIL-002 criterion 3. `make_speed_calc_decorator` and `lecture_steps` are additions for tests and screenshots, recorded in MIL-002 tasks 3 and 6 | +| 11 | Tests for new behaviour, named for behaviour, independent of order and network | Pass | `pytest -q`: 12 passed; fake clock gives exact values; no network, no shared state | +| 12 | Type checker strict, `Any` justified | Pass | `mypy src` (strict = true in `pyproject.toml`): Success, no issues in 6 source files. `Any` in `decorator.py` has the comment that a decorator accepts any function's arguments | +| 13 | Dependencies declared and pinned, none unused | Fail | Optional. `dependencies = []`; the dev extras use lower bounds (`pytest>=8`) and are not pinned. Pillow is used by the screenshot tool. Non-blocking; accepted for a learning exercise | +| 14 | Each piece of knowledge defined once | Pass | Loop sizes and the line format are only in `constants.py`; both demo loops are alike but change independently | +| 15 | Imports point inward, no cycles | Pass | `constants` <- `decorator` <- `demo_functions` <- `__main__`/`lecture_steps`; no cycle, no framework in business modules | +| 16 | SOLID holds | Pass | The decorator receives clock and output (dependency inversion); each module has one reason to change | +| 17 | Clean architecture layers and folders | Pass | Flat package `speed_calc` with a flat `tests/`, by decision in [ADR-0001] (layers would hide the three functions the course asks for); the criterion accepts a merged layer when an ADR says why. Imports still point inward | +| 18 | File and folder names English, no space, language casing | Pass | `decorator.py`, `demo_functions.py`, `test_*.py`, `generate_screenshots.py`, `step-1-current-time.png`: English, no space, snake_case or hyphen | + +## MIL-002 Go criteria + +| # | Criterion | Result | Evidence | +| --- | --- | --- | --- | +| 1 | `pytest` exits 0 | Go | 12 passed in the project `.venv` (fresh-venv install not repeated) | +| 2 | `python -m speed_calc` output | Go | Printed `1793943250.9...`, then `fast_function run speed: 0.017...s` and `slow_function run speed: 0.173...s` | +| 3 | Function names | Go | `speed_calc_decorator`, `fast_function`, `slow_function`; test `test_function_names_match_the_assignment` | +| 4 | Constants only in `constants.py`; Doxygen comments | Go | No numeric or format literals elsewhere; all modules carry Doxygen comments (`doxygen Doxyfile` not run) | +| 5 | 4 screenshots referenced in README | Go | 4 PNG files in `docs/screenshots/`; README lines 97, 104, 110, 117 reference them | +| 6 | No runtime dependencies | Go | `dependencies = []` | + +## Overall Verdict + +Go. The only Fail is criterion 13, which is Optional and so not blocking. Round 1 found a Mandatory Fail on criterion 4 (unannotated tests); it was fixed and re-checked in round 2. Criteria 7 and 10 are N-A with reasons. The review is `Mode: autonomous, not independent`. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| Pin or bound the dev dependencies if the project is kept beyond the exercise (criterion 13, Optional) | S01 | 2026-11-09 | + +--- + +[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md +[MIL-002]: ../../milestones/mil-002-speed-calc-decorator.md +[ADR-0001]: ../../adr/adr-0001-skip-stories-plain-package.md diff --git a/docs/sqa/traceability-matrix.md b/docs/sqa/traceability-matrix.md index e5ed342..2bd5f10 100644 --- a/docs/sqa/traceability-matrix.md +++ b/docs/sqa/traceability-matrix.md @@ -1,88 +1,90 @@ -# Traceability Matrix - -## Metadata -| Key | Value | -| --- | --- | -| ID | TM-001 | -| CrossReference | [BC-001], [SA-001] | - -## Version History -| Date | Status | Author | Reviewer | Change | Commit | -| --- | --- | --- | --- | --- | --- | -| 2026-11-06 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version: BC, SA, PP, MIL-001 to MIL-003 and RC-001 to RC-005 | [a07d05b] | - ---- - -## Purpose - -Tracks backward/forward links between artifact instances so that the Business Case's -cross-artifact traceability success criterion is measurable. A row is added or -updated whenever an artifact instance is created or reviewed. - -## Traceability Table - -| Artifact Instance | Type | Language | Domain | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) | -| --- | --- | --- | --- | --- | --- | --- | -| [SA-001] | Stakeholder Analysis | en | it | - | [BC-001], [PP-001] | [RC-002] (Go) | -| [BC-001] | Business Case | en | it | [SA-001] | [PP-001], [MIL-001], [MIL-002], [MIL-003] | [RC-001] (Go) | -| [PP-001] | Project Plan | en | it | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003] | - (no review gate; accepted by S01 through the accepted plan) | -| [MIL-001] | Milestone / Gateway | en | it | [BC-001], [PP-001] | [MIL-002]; source code is not built yet; ADR-0001 is written | [RC-003] (Go) | -| [MIL-002] | Milestone / Gateway | en | it | [BC-001], [PP-001], [MIL-001] | [MIL-003]; source code and tests are not built yet | [RC-004] (Go) | -| [MIL-003] | Milestone / Gateway | en | it | [BC-001], [PP-001], [MIL-002] | - | [RC-005] (Go) | -| [ADR-0001] | Architecture Decision Record | n/a | n/a | [BC-001], [PP-001] | source code and tests are not built yet | [RC-006] (Go) | -| [RC-001] | SQA Review Record | - | - | [BC-001] | - | - | -| [RC-002] | SQA Review Record | - | - | [SA-001] | - | - | -| [RC-003] | SQA Review Record | - | - | [MIL-001] | - | - | -| [RC-004] | SQA Review Record | - | - | [MIL-002] | - | - | -| [RC-005] | SQA Review Record | - | - | [MIL-003] | - | - | -| [RC-006] | SQA Review Record | - | - | [ADR-0001] | - | - | - -## Concern Coverage - -No view (architecture, design or use case artifact) exists yet, and none is planned -before the milestones: the exercise is one function and the reason for skipping -user stories and use cases is recorded in ADR-0001, a task of [MIL-001] -(accepted). Each concern is delivered by a milestone Go criterion; that is -noted in the first column. There is no Architecture Description, so the gaps -are not listed there. - -| Concern (CN-ID) | Views that answer it | Decisions (ADR) | Quality check (TP) | Status | -| --- | --- | --- | --- | --- | -| CN-001 | none; delivered by [MIL-002] Go criteria 1 and 2 | none (ADR-0001 does not address it) | none | Gap | -| CN-002 | none; delivered by [MIL-002] Go criterion 1 | none (ADR-0001 does not address it) | none | Gap | -| CN-003 | none; delivered by [MIL-003] Go criteria 1 to 3 | none (ADR-0001 does not address it) | none | Gap | -| CN-004 | none; delivered by [MIL-002] Go criterion 3 | none (ADR-0001 does not address it) | none | Gap | -| CN-005 | none; delivered by [MIL-001] Go criterion 3 | none (ADR-0001 does not address it) | none | Gap | -| CN-006 | none; delivered by [MIL-001] Go criteria 3 and 5 | none (ADR-0001 does not address it) | none | Gap | -| CN-007 | none; delivered by [MIL-001] Go criterion 1 and [MIL-002] Go criterion 6 | none (ADR-0001 does not address it) | none | Gap | - -## Open Inconsistencies - -| # | Views | Inconsistency | Found in (RC-ID) | Status | Resolved in | -| --- | --- | --- | --- | --- | --- | -| 1 | [SA-001], [BC-001] | CN-007 (no runtime dependencies) was traced to Business Case objective 3 (venv and run instructions), which does not cover it | [RC-002] | Closed | [SA-001] version of 2026-11-06 | -| 2 | [BC-001] | The constraint "No runtime dependencies unless needed" contradicted success criterion 3 (0 runtime dependencies) | [RC-001] | Closed | [BC-001] version of 2026-11-06 | -| 3 | [PP-001], [MIL-002] | The plan placed `constants.py` at [MIL-001], while [MIL-002] task 1 creates it | informal review of [PP-001] (no RC) | Closed | [PP-001] version of 2026-11-06 | - -## Coverage Notes - -- Types with no instance yet: KPI, BMC, BPMN, UCD, US, UC, DM, SSD, OC, SD, DCD, ERD, TP, AD, GOV, DICT. User stories and use cases are skipped on purpose (recorded in [ADR-0001]); GOV and DICT are not in the plan, which leaves QC-LANG-001 criterion 5 without a dictionary to check against. -- `-` in Upstream means foundational; in Downstream, nothing is built on it yet; in Last Reviewed, no `RC-*` exists yet. -- All five reviews are `Mode: autonomous, not independent` and `Go`: S01 confirmed criteria 3, 5 and 9 of QC-LANG-001 as a person in chat on 2026-11-06, so BC-001, SA-001 and MIL-001 to MIL-003 are `Accepted`. - ---- - -[BC-001]: ../business-case.md -[SA-001]: ../stakeholder-analysis.md -[PP-001]: ../project-plan.md -[MIL-001]: ../milestones/mil-001-project-baseline.md -[MIL-002]: ../milestones/mil-002-speed-calc-decorator.md -[MIL-003]: ../milestones/mil-003-wiki-and-screenshots.md -[RC-001]: ./reviews/rc-001-business-case.md -[RC-002]: ./reviews/rc-002-stakeholder-analysis.md -[RC-003]: ./reviews/rc-003-mil-001-project-baseline.md -[RC-004]: ./reviews/rc-004-mil-002-speed-calc-decorator.md -[RC-005]: ./reviews/rc-005-mil-003-wiki.md -[RC-006]: ./reviews/rc-006-adr-0001.md -[ADR-0001]: ../adr/adr-0001-skip-stories-plain-package.md -[a07d05b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/054-speed-calc-decorator/commit/a07d05b385e6b831b90731aefe08f078ebb951ca +# Traceability Matrix + +## Metadata +| Key | Value | +| --- | --- | +| ID | TM-001 | +| CrossReference | [BC-001], [SA-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-11-06 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version: BC, SA, PP, MIL-001 to MIL-003 and RC-001 to RC-005 | [a07d05b] | + +--- + +## Purpose + +Tracks backward/forward links between artifact instances so that the Business Case's +cross-artifact traceability success criterion is measurable. A row is added or +updated whenever an artifact instance is created or reviewed. + +## Traceability Table + +| Artifact Instance | Type | Language | Domain | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) | +| --- | --- | --- | --- | --- | --- | --- | +| [SA-001] | Stakeholder Analysis | en | it | - | [BC-001], [PP-001] | [RC-002] (Go) | +| [BC-001] | Business Case | en | it | [SA-001] | [PP-001], [MIL-001], [MIL-002], [MIL-003] | [RC-001] (Go) | +| [PP-001] | Project Plan | en | it | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003] | - (no review gate; accepted by S01 through the accepted plan) | +| [MIL-001] | Milestone / Gateway | en | it | [BC-001], [PP-001] | [MIL-002]; source code is not built yet; ADR-0001 is written | [RC-003] (Go) | +| [MIL-002] | Milestone / Gateway | en | it | [BC-001], [PP-001], [MIL-001] | [MIL-003]; Python source and tests (reviewed in RC-007) | [RC-004] (Go) | +| [MIL-003] | Milestone / Gateway | en | it | [BC-001], [PP-001], [MIL-002] | - | [RC-005] (Go) | +| [ADR-0001] | Architecture Decision Record | n/a | n/a | [BC-001], [PP-001] | source code and tests are not built yet | [RC-006] (Go) | +| [RC-001] | SQA Review Record | - | - | [BC-001] | - | - | +| [RC-002] | SQA Review Record | - | - | [SA-001] | - | - | +| [RC-003] | SQA Review Record | - | - | [MIL-001] | - | - | +| [RC-004] | SQA Review Record | - | - | [MIL-002] | - | - | +| [RC-005] | SQA Review Record | - | - | [MIL-003] | - | - | +| [RC-006] | SQA Review Record | - | - | [ADR-0001] | - | - | +| [RC-007] | SQA Review Record | - | - | [MIL-002] | - | - | + +## Concern Coverage + +No view (architecture, design or use case artifact) exists yet, and none is planned +before the milestones: the exercise is one function and the reason for skipping +user stories and use cases is recorded in ADR-0001, a task of [MIL-001] +(accepted). Each concern is delivered by a milestone Go criterion; that is +noted in the first column. There is no Architecture Description, so the gaps +are not listed there. + +| Concern (CN-ID) | Views that answer it | Decisions (ADR) | Quality check (TP) | Status | +| --- | --- | --- | --- | --- | +| CN-001 | none; delivered by [MIL-002] Go criteria 1 and 2 | none (ADR-0001 does not address it) | none | Gap | +| CN-002 | none; delivered by [MIL-002] Go criterion 1 | none (ADR-0001 does not address it) | none | Gap | +| CN-003 | none; delivered by [MIL-003] Go criteria 1 to 3 | none (ADR-0001 does not address it) | none | Gap | +| CN-004 | none; delivered by [MIL-002] Go criterion 3 | none (ADR-0001 does not address it) | none | Gap | +| CN-005 | none; delivered by [MIL-001] Go criterion 3 | none (ADR-0001 does not address it) | none | Gap | +| CN-006 | none; delivered by [MIL-001] Go criteria 3 and 5 | none (ADR-0001 does not address it) | none | Gap | +| CN-007 | none; delivered by [MIL-001] Go criterion 1 and [MIL-002] Go criterion 6 | none (ADR-0001 does not address it) | none | Gap | + +## Open Inconsistencies + +| # | Views | Inconsistency | Found in (RC-ID) | Status | Resolved in | +| --- | --- | --- | --- | --- | --- | +| 1 | [SA-001], [BC-001] | CN-007 (no runtime dependencies) was traced to Business Case objective 3 (venv and run instructions), which does not cover it | [RC-002] | Closed | [SA-001] version of 2026-11-06 | +| 2 | [BC-001] | The constraint "No runtime dependencies unless needed" contradicted success criterion 3 (0 runtime dependencies) | [RC-001] | Closed | [BC-001] version of 2026-11-06 | +| 3 | [PP-001], [MIL-002] | The plan placed `constants.py` at [MIL-001], while [MIL-002] task 1 creates it | informal review of [PP-001] (no RC) | Closed | [PP-001] version of 2026-11-06 | + +## Coverage Notes + +- Types with no instance yet: KPI, BMC, BPMN, UCD, US, UC, DM, SSD, OC, SD, DCD, ERD, TP, AD, GOV, DICT. User stories and use cases are skipped on purpose (recorded in [ADR-0001]); GOV and DICT are not in the plan, which leaves QC-LANG-001 criterion 5 without a dictionary to check against. +- `-` in Upstream means foundational; in Downstream, nothing is built on it yet; in Last Reviewed, no `RC-*` exists yet. +- All five reviews are `Mode: autonomous, not independent` and `Go`: S01 confirmed criteria 3, 5 and 9 of QC-LANG-001 as a person in chat on 2026-11-06, so BC-001, SA-001 and MIL-001 to MIL-003 are `Accepted`. + +--- + +[BC-001]: ../business-case.md +[SA-001]: ../stakeholder-analysis.md +[PP-001]: ../project-plan.md +[MIL-001]: ../milestones/mil-001-project-baseline.md +[MIL-002]: ../milestones/mil-002-speed-calc-decorator.md +[MIL-003]: ../milestones/mil-003-wiki-and-screenshots.md +[RC-001]: ./reviews/rc-001-business-case.md +[RC-002]: ./reviews/rc-002-stakeholder-analysis.md +[RC-003]: ./reviews/rc-003-mil-001-project-baseline.md +[RC-004]: ./reviews/rc-004-mil-002-speed-calc-decorator.md +[RC-005]: ./reviews/rc-005-mil-003-wiki.md +[RC-006]: ./reviews/rc-006-adr-0001.md +[RC-007]: ./reviews/rc-007-python-source-mil-002.md +[ADR-0001]: ../adr/adr-0001-skip-stories-plain-package.md +[a07d05b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/054-speed-calc-decorator/commit/a07d05b385e6b831b90731aefe08f078ebb951ca diff --git a/pyproject.toml b/pyproject.toml index 66505d2..322253a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ keywords = ["python", "decorator", "udemy", "100-days-of-code", "timing"] dependencies = [] [project.optional-dependencies] -dev = ["pytest>=8", "pillow>=11"] +dev = ["pytest>=8", "pillow>=11", "ruff>=0.8", "mypy>=1.13"] [tool.setuptools.packages.find] where = ["src"] @@ -27,3 +27,10 @@ addopts = "-ra" [tool.ruff] line-length = 88 + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "UP", "B"] + +[tool.mypy] +strict = true +mypy_path = "src" diff --git a/src/speed_calc/__init__.py b/src/speed_calc/__init__.py new file mode 100644 index 0000000..46ae325 --- /dev/null +++ b/src/speed_calc/__init__.py @@ -0,0 +1,14 @@ +"""! +@file __init__.py +@brief Public API of the speed calc package. +""" + +from speed_calc.decorator import make_speed_calc_decorator, speed_calc_decorator +from speed_calc.demo_functions import fast_function, slow_function + +__all__ = [ + "fast_function", + "make_speed_calc_decorator", + "slow_function", + "speed_calc_decorator", +] diff --git a/src/speed_calc/__main__.py b/src/speed_calc/__main__.py new file mode 100644 index 0000000..5dce32d --- /dev/null +++ b/src/speed_calc/__main__.py @@ -0,0 +1,21 @@ +"""! +@file __main__.py +@brief Entry point: python -m speed_calc. +""" + +import time + +from speed_calc.demo_functions import fast_function, slow_function + + +def main() -> None: + """! + @brief Print the current time, then the run time of both functions. + """ + print(time.time()) # seconds since Jan 1st, 1970 + fast_function() + slow_function() + + +if __name__ == "__main__": + main() diff --git a/src/speed_calc/constants.py b/src/speed_calc/constants.py new file mode 100644 index 0000000..2d4f62f --- /dev/null +++ b/src/speed_calc/constants.py @@ -0,0 +1,13 @@ +"""! +@file constants.py +@brief Constants used by the speed calc package. +""" + +## Number of loop iterations of fast_function. +FAST_ITERATIONS: int = 1_000_000 + +## Number of loop iterations of slow_function. +SLOW_ITERATIONS: int = 10_000_000 + +## Line format printed by the decorator: function name, then seconds. +RESULT_TEMPLATE: str = "{name} run speed: {seconds}s" diff --git a/src/speed_calc/decorator.py b/src/speed_calc/decorator.py new file mode 100644 index 0000000..ba31737 --- /dev/null +++ b/src/speed_calc/decorator.py @@ -0,0 +1,51 @@ +"""! +@file decorator.py +@brief The speed calc decorator: prints how long a function takes to run. +""" + +import functools +import time +from collections.abc import Callable +from typing import Any # Any: a decorator must accept the arguments of any function + +from speed_calc.constants import RESULT_TEMPLATE + + +def make_speed_calc_decorator( + clock: Callable[[], float] = time.time, + output: Callable[[str], None] = print, +) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """! + @brief Build a speed calc decorator with its own clock and output. + + Tests pass a fake clock, so they never depend on real elapsed time. + + @param clock Returns the current time in seconds. + @param output Receives the result line. + @return A decorator that times the function it wraps. + """ + + def decorator(function: Callable[..., Any]) -> Callable[..., Any]: + @functools.wraps(function) + def wrapper(*args: Any, **kwargs: Any) -> Any: + start = clock() + result = function(*args, **kwargs) + seconds = clock() - start + output(RESULT_TEMPLATE.format(name=function.__name__, seconds=seconds)) + return result + + return wrapper + + return decorator + + +def speed_calc_decorator(function: Callable[..., Any]) -> Callable[..., Any]: + """! + @brief Print the seconds that the decorated function takes to run. + + Use it as `@speed_calc_decorator` above a function definition. + + @param function The function to time. + @return A wrapper that behaves like @p function and prints its run time. + """ + return make_speed_calc_decorator()(function) diff --git a/src/speed_calc/demo_functions.py b/src/speed_calc/demo_functions.py new file mode 100644 index 0000000..c9fe606 --- /dev/null +++ b/src/speed_calc/demo_functions.py @@ -0,0 +1,25 @@ +"""! +@file demo_functions.py +@brief The two functions of the assignment, timed by the decorator. +""" + +from speed_calc.constants import FAST_ITERATIONS, SLOW_ITERATIONS +from speed_calc.decorator import speed_calc_decorator + + +@speed_calc_decorator +def fast_function() -> None: + """! + @brief Loop FAST_ITERATIONS times. + """ + for i in range(FAST_ITERATIONS): + i * i + + +@speed_calc_decorator +def slow_function() -> None: + """! + @brief Loop SLOW_ITERATIONS times. + """ + for i in range(SLOW_ITERATIONS): + i * i diff --git a/src/speed_calc/lecture_steps.py b/src/speed_calc/lecture_steps.py new file mode 100644 index 0000000..29d3450 --- /dev/null +++ b/src/speed_calc/lecture_steps.py @@ -0,0 +1,78 @@ +"""! +@file lecture_steps.py +@brief The four lecture steps, each runnable on its own. + +Run one with `python -m speed_calc.lecture_steps N` (N is 1 to 4). +""" + +import functools +import sys +import time +from collections.abc import Callable +from typing import Any + +from speed_calc.decorator import make_speed_calc_decorator +from speed_calc.demo_functions import fast_function + +## The undecorated fast_function; functools.wraps keeps it in `__wrapped__`. +_fast = fast_function.__wrapped__ # type: ignore[attr-defined] + + +def step_1() -> None: + """! + @brief Step 1: print the seconds since Jan 1st, 1970. + """ + print(time.time()) + + +def step_2() -> None: + """! + @brief Step 2: a decorator skeleton that only announces the call. + """ + + def skeleton(function: Callable[..., Any]) -> Callable[..., Any]: + @functools.wraps(function) + def wrapper(*args: Any, **kwargs: Any) -> Any: + print(f"calling {function.__name__}") + return function(*args, **kwargs) + + return wrapper + + skeleton(_fast)() + print(f"name kept by functools.wraps: {skeleton(_fast).__name__}") + + +def step_3() -> None: + """! + @brief Step 3: the wrapper reads the clock before and after the call. + """ + make_speed_calc_decorator()(_fast)() + + +def step_4() -> None: + """! + @brief Step 4: the finished program, fast_function against slow_function. + """ + from speed_calc.__main__ import main + + main() + + +STEPS: dict[int, Callable[[], None]] = {1: step_1, 2: step_2, 3: step_3, 4: step_4} + + +def main(argv: list[str]) -> int: + """! + @brief Run the lecture step named in @p argv. + @param argv Command line arguments without the program name. + @return Exit code: 0 on success, 2 on a bad step number. + """ + if len(argv) != 1 or not argv[0].isdigit() or int(argv[0]) not in STEPS: + print("usage: python -m speed_calc.lecture_steps N (N is 1 to 4)") + return 2 + STEPS[int(argv[0])]() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tests/test_decorator.py b/tests/test_decorator.py new file mode 100644 index 0000000..55b12c2 --- /dev/null +++ b/tests/test_decorator.py @@ -0,0 +1,60 @@ +"""Tests for the speed calc decorator; a fake clock replaces real time.""" + +from collections.abc import Callable + +import pytest + +from speed_calc.decorator import make_speed_calc_decorator + + +def make_clock(*times: float) -> Callable[[], float]: + values = iter(times) + return lambda: next(values) + + +def test_prints_function_name_and_elapsed_seconds() -> None: + lines: list[str] = [] + decorate = make_speed_calc_decorator(make_clock(10.0, 12.5), lines.append) + + @decorate + def work() -> None: + pass + + work() + + assert lines == ["work run speed: 2.5s"] + + +def test_returns_the_result_of_the_function() -> None: + decorate = make_speed_calc_decorator(make_clock(0.0, 1.0), lambda _: None) + + @decorate + def add(a: int, b: int) -> int: + return a + b + + assert add(2, b=3) == 5 + + +def test_keeps_name_and_docstring() -> None: + decorate = make_speed_calc_decorator(make_clock(0.0, 1.0), lambda _: None) + + @decorate + def documented() -> None: + """Doc.""" + + assert documented.__name__ == "documented" + assert documented.__doc__ == "Doc." + + +def test_default_decorator_prints_to_stdout(capsys: pytest.CaptureFixture[str]) -> None: + from speed_calc import speed_calc_decorator + + @speed_calc_decorator + def quick() -> None: + pass + + quick() + + out = capsys.readouterr().out + assert out.startswith("quick run speed: ") + assert out.endswith("s\n") diff --git a/tests/test_demo_functions.py b/tests/test_demo_functions.py new file mode 100644 index 0000000..d565b66 --- /dev/null +++ b/tests/test_demo_functions.py @@ -0,0 +1,33 @@ +"""Tests for the assignment's functions and the entry point.""" + +import re + +import pytest + +from speed_calc import constants, fast_function, slow_function +from speed_calc.__main__ import main + +LINE = r"(fast|slow)_function run speed: \d+(\.\d+)?(e-?\d+)?s" + + +def test_function_names_match_the_assignment() -> None: + assert fast_function.__name__ == "fast_function" + assert slow_function.__name__ == "slow_function" + + +def test_constants_hold_the_loop_sizes() -> None: + assert constants.FAST_ITERATIONS == 1_000_000 + assert constants.SLOW_ITERATIONS == 10_000_000 + + +def test_main_prints_time_then_fast_then_slow( + capsys: pytest.CaptureFixture[str], +) -> None: + main() + + lines = capsys.readouterr().out.splitlines() + + assert len(lines) == 3 + assert float(lines[0]) > 0 + assert re.fullmatch(LINE, lines[1]) and lines[1].startswith("fast_function") + assert re.fullmatch(LINE, lines[2]) and lines[2].startswith("slow_function") diff --git a/tests/test_lecture_steps.py b/tests/test_lecture_steps.py new file mode 100644 index 0000000..45da9a2 --- /dev/null +++ b/tests/test_lecture_steps.py @@ -0,0 +1,36 @@ +"""Tests for the lecture steps.""" + +import pytest + +from speed_calc.lecture_steps import main + + +def test_step_1_prints_the_current_time(capsys: pytest.CaptureFixture[str]) -> None: + assert main(["1"]) == 0 + assert float(capsys.readouterr().out) > 0 + + +def test_step_2_announces_the_call_and_keeps_the_name( + capsys: pytest.CaptureFixture[str], +) -> None: + assert main(["2"]) == 0 + lines = capsys.readouterr().out.splitlines() + assert lines == [ + "calling fast_function", + "name kept by functools.wraps: fast_function", + ] + + +def test_step_3_prints_one_timing_line(capsys: pytest.CaptureFixture[str]) -> None: + assert main(["3"]) == 0 + assert capsys.readouterr().out.startswith("fast_function run speed: ") + + +def test_step_4_prints_three_lines(capsys: pytest.CaptureFixture[str]) -> None: + assert main(["4"]) == 0 + assert len(capsys.readouterr().out.splitlines()) == 3 + + +def test_bad_step_number_is_refused(capsys: pytest.CaptureFixture[str]) -> None: + assert main(["9"]) == 2 + assert "usage" in capsys.readouterr().out -- 2.54.0 From 8daf42920ef9273a49f9859323a0712aa8824388 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Fri, 6 Nov 2026 13:35:32 +0800 Subject: [PATCH 2/3] Resolve pending commit links --- docs/sqa/reviews/rc-007-python-source-mil-002.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sqa/reviews/rc-007-python-source-mil-002.md b/docs/sqa/reviews/rc-007-python-source-mil-002.md index 3918c02..b17267e 100644 --- a/docs/sqa/reviews/rc-007-python-source-mil-002.md +++ b/docs/sqa/reviews/rc-007-python-source-mil-002.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-11-06 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | +| 2026-11-06 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [d7c4532] | --- @@ -71,3 +71,4 @@ Go. The only Fail is criterion 13, which is Optional and so not blocking. Round [QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md [MIL-002]: ../../milestones/mil-002-speed-calc-decorator.md [ADR-0001]: ../../adr/adr-0001-skip-stories-plain-package.md +[d7c4532]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/054-speed-calc-decorator/commit/d7c45320d545b47d45d65174544101cdbe11f641 -- 2.54.0 From 0752fbd06692aead5c31f7d2dc85208006b10821 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Fri, 6 Nov 2026 13:40:28 +0800 Subject: [PATCH 3/3] Render screenshots in-process instead of through a subprocess The OpenGrep scan flagged subprocess.run with a non-literal command in the screenshot script. The script now runs each step in the same process. Task: MIL-002#6 --- docs/screenshots/generate_screenshots.py | 162 ++++++++++----------- docs/screenshots/step-1-current-time.png | Bin 6239 -> 6317 bytes docs/screenshots/step-3-timing-wrapper.png | Bin 9350 -> 9129 bytes docs/screenshots/step-4-fast-vs-slow.png | Bin 15937 -> 16224 bytes 4 files changed, 76 insertions(+), 86 deletions(-) diff --git a/docs/screenshots/generate_screenshots.py b/docs/screenshots/generate_screenshots.py index 1cb6972..b4d72a3 100644 --- a/docs/screenshots/generate_screenshots.py +++ b/docs/screenshots/generate_screenshots.py @@ -1,86 +1,76 @@ -"""Render the real output of each lecture step to a PNG file. - -Dev-only tool (needs Pillow, installed by `pip install -e ".[dev]"`). It runs -each command, captures its output and draws it as a terminal image, so no -screen is captured and the desktop is never touched. - -Usage, from the project root: python docs/screenshots/generate_screenshots.py -""" - -import subprocess -import sys -from pathlib import Path - -from PIL import Image, ImageDraw, ImageFont - -OUTPUT_DIR = Path(__file__).resolve().parent -ROOT = OUTPUT_DIR.parent.parent -PYTHON = "python" - -# (file name, command shown in the prompt, arguments run after the interpreter) -SHOTS = [ - ( - "step-1-current-time.png", - "python -m speed_calc.lecture_steps 1", - ["-m", "speed_calc.lecture_steps", "1"], - ), - ( - "step-2-decorator-skeleton.png", - "python -m speed_calc.lecture_steps 2", - ["-m", "speed_calc.lecture_steps", "2"], - ), - ( - "step-3-timing-wrapper.png", - "python -m speed_calc.lecture_steps 3", - ["-m", "speed_calc.lecture_steps", "3"], - ), - ("step-4-fast-vs-slow.png", "python -m speed_calc", ["-m", "speed_calc"]), -] - -BACKGROUND, FOREGROUND, PROMPT = "#1e1e2e", "#cdd6f4", "#a6e3a1" -FONT_CANDIDATES = [ - "C:/Windows/Fonts/consola.ttf", - "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", - "/System/Library/Fonts/Menlo.ttc", -] - - -def load_font(size: int) -> ImageFont.ImageFont: - for candidate in FONT_CANDIDATES: - if Path(candidate).exists(): - return ImageFont.truetype(candidate, size) - return ImageFont.load_default() - - -def run(arguments: list[str]) -> str: - done = subprocess.run( - [sys.executable, *arguments], - cwd=ROOT, - capture_output=True, - text=True, - check=True, - ) - return done.stdout.strip() - - -def render(path: Path, command: str, output: str) -> None: - font = load_font(20) - lines = [f"$ {command}", *output.splitlines()] - width = int(max(font.getlength(line) for line in lines)) + 48 - height = 28 * len(lines) + 48 - image = Image.new("RGB", (max(width, 640), height), BACKGROUND) - draw = ImageDraw.Draw(image) - for index, line in enumerate(lines): - colour = PROMPT if index == 0 else FOREGROUND - draw.text((24, 24 + 28 * index), line, font=font, fill=colour) - image.save(path) - - -def main() -> None: - for name, command, arguments in SHOTS: - render(OUTPUT_DIR / name, command, run(arguments)) - print("wrote", name) - - -if __name__ == "__main__": - main() +"""Render the real output of each lecture step to a PNG file. + +Dev-only tool (needs Pillow, installed by `pip install -e ".[dev]"`). It runs +each step in-process, captures its output and draws it as a terminal image, so no +screen is captured and the desktop is never touched. + +Usage, from the project root: python docs/screenshots/generate_screenshots.py +""" + +import contextlib +import io +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +from speed_calc.__main__ import main as speed_calc_main +from speed_calc.lecture_steps import main as lecture_steps_main + +OUTPUT_DIR = Path(__file__).resolve().parent +ROOT = OUTPUT_DIR.parent.parent + +# (file name, command shown in the prompt, lecture step to run; None = the program) +SHOTS = [ + ("step-1-current-time.png", "python -m speed_calc.lecture_steps 1", ["1"]), + ("step-2-decorator-skeleton.png", "python -m speed_calc.lecture_steps 2", ["2"]), + ("step-3-timing-wrapper.png", "python -m speed_calc.lecture_steps 3", ["3"]), + ("step-4-fast-vs-slow.png", "python -m speed_calc", None), +] + +BACKGROUND, FOREGROUND, PROMPT = "#1e1e2e", "#cdd6f4", "#a6e3a1" +FONT_CANDIDATES = [ + "C:/Windows/Fonts/consola.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + "/System/Library/Fonts/Menlo.ttc", +] + + +def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + for candidate in FONT_CANDIDATES: + if Path(candidate).exists(): + return ImageFont.truetype(candidate, size) + return ImageFont.load_default() + + +def run(step: list[str] | None) -> str: + """Run a step in this process and return what it printed.""" + buffer = io.StringIO() + with contextlib.redirect_stdout(buffer): + if step is None: + speed_calc_main() + else: + lecture_steps_main(step) + return buffer.getvalue().strip() + + +def render(path: Path, command: str, output: str) -> None: + font = load_font(20) + lines = [f"$ {command}", *output.splitlines()] + width = int(max(font.getlength(line) for line in lines)) + 48 + height = 28 * len(lines) + 48 + image = Image.new("RGB", (max(width, 640), height), BACKGROUND) + draw = ImageDraw.Draw(image) + for index, line in enumerate(lines): + colour = PROMPT if index == 0 else FOREGROUND + draw.text((24, 24 + 28 * index), line, font=font, fill=colour) + image.save(path) + + +def main() -> None: + for name, command, step in SHOTS: + render(OUTPUT_DIR / name, command, run(step)) + print("wrote", name) + + +if __name__ == "__main__": + main() diff --git a/docs/screenshots/step-1-current-time.png b/docs/screenshots/step-1-current-time.png index c8181284a2d3abd49313fdcc4d85b2c5a4564890..3f6f091b082857c53c4c5c65cef83cab1ac57242 100644 GIT binary patch delta 5953 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 PuL4qgsjX70Y!mTc>X7_B delta 5873 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 Z!eJuenTT0)rururN^)S?3Tcbb{{;(kN=X0! literal 9350 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 E3&5)3bN~PV diff --git a/docs/screenshots/step-4-fast-vs-slow.png b/docs/screenshots/step-4-fast-vs-slow.png index 5326d11347003fd7df700a0af0479212921df62e..b7c5c6faa3bd6868a27cce49af38431ad993f30c 100644 GIT binary patch literal 16224 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 DXPbp9 -- 2.54.0