MIL-002: speed calc decorator #20

Merged
Tirsvad merged 3 commits from mil-002-speed-calc-decorator into main 2026-11-06 06:43:20 +01:00
19 changed files with 754 additions and 224 deletions

No files matched your search

+174 -134
View File
@@ -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
+1 -1
View File
@@ -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
+76
View File
@@ -0,0 +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 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()
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,74 @@
# 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 | [d7c4532] |
---
## 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
[d7c4532]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/054-speed-calc-decorator/commit/d7c45320d545b47d45d65174544101cdbe11f641
+90 -88
View File
@@ -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
+8 -1
View File
@@ -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"
+14
View File
@@ -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",
]
+21
View File
@@ -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()
+13
View File
@@ -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"
+51
View File
@@ -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)
+25
View File
@@ -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
+78
View File
@@ -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:]))
+60
View File
@@ -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")
+33
View File
@@ -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")
+36
View File
@@ -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