Compare commits
13
Commits
d8b08cb0b3
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0904993de5 | ||
|
|
6bccc8c9e7 | ||
|
|
afdffdf564 | ||
|
|
db477d99fb | ||
|
|
142a027e5c | ||
|
|
6d99810e30 | ||
|
|
9030510773 | ||
|
|
5e1067b4f3 | ||
|
|
c90e5afb7c | ||
|
|
1237f41840 | ||
|
|
6809db7930 | ||
|
|
31c713e995 | ||
|
|
8ccefffef7 |
+177
-175
@@ -1,176 +1,178 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Doxygen output
|
||||
docs/doxygen/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "framework"]
|
||||
path = framework
|
||||
url = ssh://git@git.tirsystem.com:10022/TirSystem/SQA-QC-Framework.git
|
||||
@@ -0,0 +1,16 @@
|
||||
# Doxygen configuration for the console calculator.
|
||||
# Generate the API documentation with: doxygen Doxyfile
|
||||
PROJECT_NAME = "Calculator"
|
||||
PROJECT_BRIEF = "Console calculator (Udemy 100 Days of Code, day 10)"
|
||||
OUTPUT_DIRECTORY = docs/doxygen
|
||||
INPUT = src
|
||||
FILE_PATTERNS = *.py
|
||||
RECURSIVE = YES
|
||||
OPTIMIZE_OUTPUT_JAVA = YES
|
||||
EXTRACT_ALL = YES
|
||||
EXTRACT_PRIVATE = NO
|
||||
GENERATE_HTML = YES
|
||||
GENERATE_LATEX = NO
|
||||
QUIET = YES
|
||||
WARN_IF_UNDOCUMENTED = YES
|
||||
WARN_NO_PARAMDOC = YES
|
||||
@@ -1,2 +1,92 @@
|
||||
# 010-calc
|
||||
# 🧮 Calculator
|
||||
|
||||
A beginner-friendly Python console program that adds, subtracts, multiplies and divides numbers, and lets you keep calculating with the previous result.
|
||||
|
||||
## 📚 Table of Contents
|
||||
|
||||
- [Overview](#-overview)
|
||||
- [Requirements](#-requirements)
|
||||
- [Setup](#-setup)
|
||||
- [Run](#-run)
|
||||
- [Tests](#-tests)
|
||||
- [License](#-license)
|
||||
- [Links](#-links)
|
||||
|
||||
## 🧭 Overview
|
||||
|
||||
Final project of Day 10 of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*. The program shows an ASCII calculator with the title beside it, then asks for a number, an operation (`+`, `-`, `*`, `/`) and a second number.
|
||||
|
||||
The operations are functions stored in a dictionary (`OPERATIONS` in `src/calc/operations.py`) and called through it. After each result you can continue with it (`y`), start a new calculation (`n`, which calls `calculator()` again through recursion) or quit (`q`). Invalid input is asked for again, and dividing by zero starts over with a message.
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- Python 3.13 or newer
|
||||
- No runtime dependencies
|
||||
- `pytest` for the tests (installed with the `dev` extra)
|
||||
- Optional: [Doxygen](https://www.doxygen.nl/) to build the API documentation
|
||||
|
||||
## 🛠️ Setup
|
||||
|
||||
Create a local virtual environment in the project folder:
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
```
|
||||
|
||||
Activate it:
|
||||
|
||||
```bash
|
||||
# Windows (PowerShell)
|
||||
.venv\Scripts\Activate.ps1
|
||||
|
||||
# Windows (Git Bash)
|
||||
source .venv/Scripts/activate
|
||||
|
||||
# Linux / macOS
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
Upgrade pip and 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`. The `.env` file in the repository is for personal tooling only; the project never reads or imports it.
|
||||
|
||||
## ▶️ Run
|
||||
|
||||
With the environment active:
|
||||
|
||||
```bash
|
||||
python -m calc
|
||||
```
|
||||
|
||||
or, since the install adds a console script, simply:
|
||||
|
||||
```bash
|
||||
calc
|
||||
```
|
||||
|
||||
## 🧪 Tests
|
||||
|
||||
```bash
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
Build the API documentation from the Doxygen comments (written to `docs/doxygen/`):
|
||||
|
||||
```bash
|
||||
doxygen Doxyfile
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
GNU Affero General Public License v3.0, see [LICENSE](LICENSE).
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Repository](https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code)
|
||||
- [Documentation](docs/doxygen/html/index.html) (generated with `doxygen Doxyfile`)
|
||||
- [Issue tracker](https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/issues)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Artifact Registry
|
||||
|
||||
This project's artifact state. Types, short names and `CrossReference
|
||||
Candidates` come from the framework catalog
|
||||
(`framework/registry/artifact-catalog.md`); this file only records where
|
||||
each document lives in *this* project and the next version to use.
|
||||
|
||||
Delete rows for types you don't use. Add a row the first time you create a
|
||||
document of a type. `Primary File` may contain a glob (e.g.
|
||||
`docs/uc-*/uc.md`); `framework/scripts/find-crossreferences.sh` reads it.
|
||||
|
||||
| Short Name | Artifact Type | Primary File | Next Available Version |
|
||||
| --- | --- | --- | --- |
|
||||
| PP | Project Plan | docs/project-plan.md | 002 |
|
||||
| MIL | Milestone / Gateway | docs/milestones/*.md | 005 |
|
||||
| BC | Business Case | docs/business-case.md | 002 |
|
||||
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
||||
|
||||
## Languages
|
||||
|
||||
Set the PO language when the project starts; `project-planning` asks for it
|
||||
if it is missing. A translated artifact is named `<artifact>.<language>.md`
|
||||
(for example `business-case.da.md`); the English file stays the source.
|
||||
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| PO language | en |
|
||||
| High-level register | IT Executive English |
|
||||
| Technical register | IT Professional English |
|
||||
|
||||
| Artifact types | Register | Also kept as a PO-language file |
|
||||
| --- | --- | --- |
|
||||
| BC, KPI, PP, MIL | IT Executive English | Yes |
|
||||
| SA, BMC, BPMN, UCD, US, UC, SSD, DM, RA, GOV, DICT | IT Professional English | Yes |
|
||||
| OC, SD, DCD, ERD, ADR, TM, RC, QC, source code | IT Professional English | No |
|
||||
|
||||
## Notes
|
||||
|
||||
- "Next Available Version" is the zero-padded (3-digit) version to use the
|
||||
*next* time a new document of that type is created. Increment it only when
|
||||
a brand-new document is created, not when an existing document's
|
||||
`## Version History` gets a row.
|
||||
- `ADR` uses 4 digits (`0001`); `RC` is sequential across all artifact types.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Business Case
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | BC-001 |
|
||||
| CrossReference | [SA-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [8ccefff] |
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Day 10 of the Udemy course "100 Days of Code: The Complete Python Pro Bootcamp" asks for a console calculator that uses functions, a dictionary of functions, user input, a loop and recursion. This project delivers it as a small, tested and documented Python 3.13+ program with no runtime dependencies.
|
||||
|
||||
## Methodological and Standards Foundation
|
||||
|
||||
The SQA and QC framework in `framework/` (plan first, then code), ISO/IEC 25010:2023 for quality characteristics, and the Python conventions of the `coding-conventions` skill.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The assignment needs a finished, verifiable result with setup and run instructions, not a single untested script.
|
||||
|
||||
## Business Opportunity
|
||||
|
||||
A compact reference project that shows the lecture concepts and can serve as the template for later course days.
|
||||
|
||||
## Objectives
|
||||
|
||||
1. Add, subtract, multiply and divide two numbers from console input.
|
||||
2. Store the operations as functions in a dictionary and call them through it.
|
||||
3. Let the user continue with the previous result or start a new calculation, restarting by recursion.
|
||||
4. Cover the behaviour with automated tests and document setup, run and tests in the README.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
Console program under `src/`, tests under `tests/`, `pyproject.toml`, `Doxyfile`, README, ASCII title and calculator, repository description and topics.
|
||||
|
||||
### Out of Scope
|
||||
|
||||
Graphical or web interface, further operations, packaging for PyPI, runtime dependencies.
|
||||
|
||||
## Expected Benefits
|
||||
|
||||
### Tangible Benefits
|
||||
|
||||
A working, tested calculator and a reproducible setup with a local `.venv`.
|
||||
|
||||
### Intangible Benefits
|
||||
|
||||
Practice with the plan-first process on a small project.
|
||||
|
||||
## Strategic Alignment
|
||||
|
||||
Supports completing the Python Pro Bootcamp with reviewed, portfolio-quality projects.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
| # | Criterion | Target | Measure |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Operations and loop work | All tests pass | `python -m pytest` |
|
||||
| 2 | Setup is reproducible | README steps work on a clean `.venv` | Manual run of the README |
|
||||
| 3 | No runtime dependencies | Empty dependency list | `pyproject.toml` |
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
| --- | --- | --- |
|
||||
| Doxygen is not installed locally | Docs build not checked | README names it as optional |
|
||||
| Token in `.env` leaks | Account compromise | `.env` stays git-ignored and is never read by the project |
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Python 3.13 or newer is installed.
|
||||
- The git host is reachable for issues and pull requests.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Python 3.13 or newer, `venv`, folders `src/`, `tests/`, `docs/`, constants in `constants.py`.
|
||||
- No commit or push without the Product Owner's request.
|
||||
|
||||
## Cost–Benefit Assessment
|
||||
|
||||
| Costs | Benefits |
|
||||
| --- | --- |
|
||||
| A few hours of the Product Owner's time | A finished, documented assignment and a reusable project skeleton |
|
||||
|
||||
## Stakeholders
|
||||
|
||||
| Stakeholder ID (SA) | Interest in this project |
|
||||
| --- | --- |
|
||||
| S01 | Owns the assignment, does the work and accepts each milestone |
|
||||
|
||||
## Recommendation
|
||||
|
||||
Proceed — the scope is small, the cost is low and it completes the assignment.
|
||||
|
||||
---
|
||||
|
||||
[SA-001]: ./stakeholder-analysis.md
|
||||
[8ccefff]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/commit/8ccefffef7ec1f9ecbf548085a5c92fae75e1341
|
||||
@@ -0,0 +1,70 @@
|
||||
# MIL-001 Project scaffold
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | MIL-001 |
|
||||
| CrossReference | [PP-001], [SA-001], [BC-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [8ccefff] |
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Decide whether the repository is ready for code: layout, tooling and configuration exist and a fresh clone can create a local virtual environment.
|
||||
|
||||
## Deliverable
|
||||
|
||||
`pyproject.toml`, Python `.gitignore`, `src/calc/` and `tests/` skeletons, `Doxyfile`, repository description and topics on the git host.
|
||||
|
||||
## Go / No-Go Criteria
|
||||
|
||||
| # | Criterion (objectively checkable) | Go | No-Go |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `pyproject.toml` requires Python 3.13 or newer and has no runtime dependencies | Met | Not met |
|
||||
| 2 | `.gitignore` covers `.venv`, `.env` and `docs/doxygen/` | Met | Not met |
|
||||
| 3 | Folders `src/`, `tests/` and `docs/` exist | Met | Not met |
|
||||
| 4 | Repository description and topics are set on the git host | Met | Not met |
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Depends on | Reason |
|
||||
| --- | --- |
|
||||
| None | First milestone |
|
||||
|
||||
## Traceability
|
||||
|
||||
| Business Case objective / KPI / user story | Reference |
|
||||
| --- | --- |
|
||||
| Objectives 1 to 4 | [BC-001] |
|
||||
|
||||
## Ownership
|
||||
|
||||
| Role | Stakeholder ID (SA) |
|
||||
| --- | --- |
|
||||
| Owner | S01 |
|
||||
| Approving reviewer | S01 |
|
||||
|
||||
## Target Date
|
||||
|
||||
2026-10-05
|
||||
|
||||
## Tasks
|
||||
|
||||
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | Add pyproject.toml | Project configuration: name, version, Python 3.13+ requirement, empty runtime dependencies, `dev` extra with pytest, console script `calc`, pytest settings. | No | |
|
||||
| 2 | Add Python .gitignore and folder layout | Python `.gitignore` that also ignores `.env`, `.venv` and `docs/doxygen/`; create `src/calc/` and `tests/`. | No | |
|
||||
| 3 | Add Doxyfile | Doxygen configuration reading `src/`, writing to `docs/doxygen/`, so Doxygen-style comments can be built into API docs. | No | |
|
||||
| 4 | Set repository description and topics | Set the description and topics on the git host repository through its API, with the token from `.env`, which is personal tooling and never part of the project. | No | |
|
||||
|
||||
---
|
||||
|
||||
[PP-001]: ../project-plan.md
|
||||
[SA-001]: ../stakeholder-analysis.md
|
||||
[BC-001]: ../business-case.md
|
||||
[8ccefff]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/commit/8ccefffef7ec1f9ecbf548085a5c92fae75e1341
|
||||
@@ -0,0 +1,70 @@
|
||||
# MIL-002 Calculator core
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | MIL-002 |
|
||||
| CrossReference | [PP-001], [SA-001], [BC-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [8ccefff] |
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Decide whether the arithmetic core is correct and tested before the console interface is built on it.
|
||||
|
||||
## Deliverable
|
||||
|
||||
`src/calc/constants.py` and `src/calc/operations.py` with add, subtract, multiply, divide and the `OPERATIONS` dictionary, plus `tests/test_operations.py`.
|
||||
|
||||
## Go / No-Go Criteria
|
||||
|
||||
| # | Criterion (objectively checkable) | Go | No-Go |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | The four operations return correct results in the tests | Met | Not met |
|
||||
| 2 | Division by zero raises `ZeroDivisionError` | Met | Not met |
|
||||
| 3 | `OPERATIONS` maps `+ - * /` to the stored (not called) functions | Met | Not met |
|
||||
| 4 | All constants live in `constants.py` | Met | Not met |
|
||||
| 5 | Every function has a Doxygen comment | Met | Not met |
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Depends on | Reason |
|
||||
| --- | --- |
|
||||
| MIL-001 | Needs the package layout and configuration |
|
||||
|
||||
## Traceability
|
||||
|
||||
| Business Case objective / KPI / user story | Reference |
|
||||
| --- | --- |
|
||||
| Objectives 1 to 4 | [BC-001] |
|
||||
|
||||
## Ownership
|
||||
|
||||
| Role | Stakeholder ID (SA) |
|
||||
| --- | --- |
|
||||
| Owner | S01 |
|
||||
| Approving reviewer | S01 |
|
||||
|
||||
## Target Date
|
||||
|
||||
2026-10-06
|
||||
|
||||
## Tasks
|
||||
|
||||
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | Add constants module | `constants.py` holding operation symbols, answers, prompts, messages and the ASCII art, so no literals are scattered through the code. | No | |
|
||||
| 2 | Implement the four operations and the OPERATIONS dictionary | Functions `add`, `subtract`, `multiply`, `divide` and a dictionary mapping each symbol to its function, stored without calling them, as taught in the lecture. | No | |
|
||||
| 3 | Add operation tests | pytest tests for each operation, division by zero and the dictionary contents. | No | |
|
||||
|
||||
---
|
||||
|
||||
[PP-001]: ../project-plan.md
|
||||
[SA-001]: ../stakeholder-analysis.md
|
||||
[BC-001]: ../business-case.md
|
||||
[8ccefff]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/commit/8ccefffef7ec1f9ecbf548085a5c92fae75e1341
|
||||
@@ -0,0 +1,71 @@
|
||||
# MIL-003 Console interface
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | MIL-003 |
|
||||
| CrossReference | [PP-001], [SA-001], [BC-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [8ccefff] |
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Decide whether the interactive calculator behaves as the lecture describes, including continuing with the result and restarting by recursion.
|
||||
|
||||
## Deliverable
|
||||
|
||||
`src/calc/calculator.py` and `src/calc/__main__.py` with the ASCII title and calculator, input handling and the recursive restart, plus `tests/test_calculator.py`.
|
||||
|
||||
## Go / No-Go Criteria
|
||||
|
||||
| # | Criterion (objectively checkable) | Go | No-Go |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | The program asks for a number, an operation and a second number | Met | Not met |
|
||||
| 2 | Answer `y` continues with the previous result, `n` starts a new calculation by calling `calculator()` again, `q` quits | Met | Not met |
|
||||
| 3 | Invalid numbers, operations and answers are re-prompted; division by zero restarts the calculator | Met | Not met |
|
||||
| 4 | ASCII title and ASCII calculator are shown at start | Met | Not met |
|
||||
| 5 | `python -m calc` runs and the tests pass | Met | Not met |
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Depends on | Reason |
|
||||
| --- | --- |
|
||||
| MIL-002 | Uses the operations dictionary and constants |
|
||||
|
||||
## Traceability
|
||||
|
||||
| Business Case objective / KPI / user story | Reference |
|
||||
| --- | --- |
|
||||
| Objectives 1 to 4 | [BC-001] |
|
||||
|
||||
## Ownership
|
||||
|
||||
| Role | Stakeholder ID (SA) |
|
||||
| --- | --- |
|
||||
| Owner | S01 |
|
||||
| Approving reviewer | S01 |
|
||||
|
||||
## Target Date
|
||||
|
||||
2026-10-07
|
||||
|
||||
## Tasks
|
||||
|
||||
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | Implement the calculator loop with recursive restart | `calculator()` reads the numbers and operation, looks the function up in `OPERATIONS`, and lets the user continue with the result or call `calculator()` again for a new calculation. | No | |
|
||||
| 2 | Add ASCII title and ASCII calculator | Show the ASCII title and an ASCII calculator when the program starts, taken from `constants.py`. | No | |
|
||||
| 3 | Handle invalid input and division by zero | Re-prompt on non-numeric input, unknown operations and unknown answers; print a message and restart on division by zero. | No | |
|
||||
| 4 | Add interface tests | pytest tests that feed `input()` through monkeypatch and check the printed results, the continue and new paths, re-prompts and the ASCII art. | No | |
|
||||
|
||||
---
|
||||
|
||||
[PP-001]: ../project-plan.md
|
||||
[SA-001]: ../stakeholder-analysis.md
|
||||
[BC-001]: ../business-case.md
|
||||
[8ccefff]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/commit/8ccefffef7ec1f9ecbf548085a5c92fae75e1341
|
||||
@@ -0,0 +1,69 @@
|
||||
# MIL-004 Documentation and release
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | MIL-004 |
|
||||
| CrossReference | [PP-001], [SA-001], [BC-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [8ccefff] |
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Decide whether the project can be handed over: a new user can set it up, run it and test it from the README alone.
|
||||
|
||||
## Deliverable
|
||||
|
||||
`README.md` following the project template, Doxygen output building without warnings, AGPL licence referenced.
|
||||
|
||||
## Go / No-Go Criteria
|
||||
|
||||
| # | Criterion (objectively checkable) | Go | No-Go |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | README has Overview, Requirements, Setup, Run, Tests, License and Links | Met | Not met |
|
||||
| 2 | README explains creating `.venv` and `python -m pip install --upgrade pip` | Met | Not met |
|
||||
| 3 | `doxygen Doxyfile` builds with no warnings | Met | Not met |
|
||||
| 4 | Following the README on a clean clone, the program runs and the tests pass | Met | Not met |
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Depends on | Reason |
|
||||
| --- | --- |
|
||||
| MIL-003 | Documents the finished program |
|
||||
|
||||
## Traceability
|
||||
|
||||
| Business Case objective / KPI / user story | Reference |
|
||||
| --- | --- |
|
||||
| Objectives 1 to 4 | [BC-001] |
|
||||
|
||||
## Ownership
|
||||
|
||||
| Role | Stakeholder ID (SA) |
|
||||
| --- | --- |
|
||||
| Owner | S01 |
|
||||
| Approving reviewer | S01 |
|
||||
|
||||
## Target Date
|
||||
|
||||
2026-10-08
|
||||
|
||||
## Tasks
|
||||
|
||||
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | Write README.md | README from the project template with calculator-specific text: venv setup, pip upgrade, run, tests, AGPL license and links. | No | |
|
||||
| 2 | Verify Doxygen build | Run `doxygen Doxyfile` and fix any warnings so every module and function is documented. | No | |
|
||||
| 3 | Verify setup from a clean clone | Follow the README in a fresh `.venv` to confirm install, run and test commands work as written. | No | |
|
||||
|
||||
---
|
||||
|
||||
[PP-001]: ../project-plan.md
|
||||
[SA-001]: ../stakeholder-analysis.md
|
||||
[BC-001]: ../business-case.md
|
||||
[8ccefff]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/commit/8ccefffef7ec1f9ecbf548085a5c92fae75e1341
|
||||
@@ -0,0 +1,92 @@
|
||||
# Project Plan
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | PP-001 |
|
||||
| CrossReference | [MIL-001], [MIL-002], [MIL-003], [MIL-004], [SA-001], [BC-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [8ccefff] |
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Schedule the four phases that deliver the console calculator of Udemy's "100 Days of Code" day 10 assignment. This is a very small project, so the Business Case is short and the Stakeholder Analysis names only the Product Owner (S01); the plan, the milestones and their tasks are the whole planning trail. Each phase is one branch and one pull request, and each task is synced as an issue.
|
||||
|
||||
## Planning Assumptions
|
||||
|
||||
- Work starts 2026-10-05 and ends by 2026-10-08, one phase per day.
|
||||
- Owner and reviewer are the Product Owner, S01. The review is the Product Owner accepting the document.
|
||||
- Product Owner language: English.
|
||||
- Python 3.13 or newer, `venv`, `src/ tests/ docs/` layout, constants in `constants.py`, Doxygen comments.
|
||||
- `.env` holds a personal token used only for tooling; the project never reads or tests it.
|
||||
- Nothing is committed or pushed until the Product Owner asks.
|
||||
|
||||
## Gateway Schedule
|
||||
|
||||
| Gateway | Document | Window | Decision date | Owner | Stories | Main deliverable | Milestone |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| Project scaffold | [MIL-001] | 2026-10-05 | 2026-10-05 | S01 | | `pyproject.toml` | [Milestone 30] |
|
||||
| Calculator core | [MIL-002] | 2026-10-06 | 2026-10-06 | S01 | | `src/calc/constants.py` and `src/calc/operations.py` | [Milestone 31] |
|
||||
| Console interface | [MIL-003] | 2026-10-07 | 2026-10-07 | S01 | | `src/calc/calculator.py` and `src/calc/__main__.py` | [Milestone 32] |
|
||||
| Documentation and release | [MIL-004] | 2026-10-08 | 2026-10-08 | S01 | | `README.md` following the project template | [Milestone 33] |
|
||||
|
||||
```plantuml
|
||||
@startgantt
|
||||
Project starts 2026-10-05
|
||||
[Project scaffold] starts 2026-10-05 and ends 2026-10-05
|
||||
[Project scaffold Go/No-Go] happens 2026-10-05
|
||||
[Calculator core] starts 2026-10-06 and ends 2026-10-06
|
||||
[Calculator core Go/No-Go] happens 2026-10-06
|
||||
[Console interface] starts 2026-10-07 and ends 2026-10-07
|
||||
[Console interface Go/No-Go] happens 2026-10-07
|
||||
[Documentation and release] starts 2026-10-08 and ends 2026-10-08
|
||||
[Documentation and release Go/No-Go] happens 2026-10-08
|
||||
@endgantt
|
||||
```
|
||||
|
||||
## Scope Coverage
|
||||
|
||||
| Scope item | Gateway |
|
||||
| --- | --- |
|
||||
| Project configuration, layout, `.gitignore`, Doxyfile, repository metadata | [MIL-001] |
|
||||
| Operations stored in a dictionary of functions, constants, tests | [MIL-002] |
|
||||
| Console interaction, ASCII art, continue with result, recursion | [MIL-003] |
|
||||
| README, Doxygen build, setup verification, AGPL licence | [MIL-004] |
|
||||
|
||||
## Dependencies
|
||||
|
||||
```
|
||||
MIL-001 -> MIL-002 -> MIL-003 -> MIL-004
|
||||
```
|
||||
|
||||
A No-Go moves every later date by the time needed to fix the failed criterion.
|
||||
|
||||
## Plan Risks
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
| --- | --- | --- |
|
||||
| Doxygen not installed on the machine | Docs check skipped | README names it as optional |
|
||||
| Recursion depth on very long sessions | Crash after many restarts | Documented limit; sessions are short |
|
||||
|
||||
## Open Issues
|
||||
|
||||
- Milestone links in the Gateway Schedule are filled in after `sync-project.sh --apply`.
|
||||
|
||||
---
|
||||
|
||||
[MIL-001]: ./milestones/mil-001-project-scaffold.md
|
||||
[MIL-002]: ./milestones/mil-002-calculator-core.md
|
||||
[MIL-003]: ./milestones/mil-003-console-interface.md
|
||||
[MIL-004]: ./milestones/mil-004-documentation-and-release.md
|
||||
[SA-001]: ./stakeholder-analysis.md
|
||||
[BC-001]: ./business-case.md
|
||||
[8ccefff]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/commit/8ccefffef7ec1f9ecbf548085a5c92fae75e1341
|
||||
[Milestone 30]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/milestone/30
|
||||
[Milestone 31]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/milestone/31
|
||||
[Milestone 32]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/milestone/32
|
||||
[Milestone 33]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/milestone/33
|
||||
@@ -0,0 +1,64 @@
|
||||
# Stakeholder Analysis
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | SA-001 |
|
||||
| CrossReference | [BC-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-04 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version | [8ccefff] |
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Name the one stakeholder of this very small project, so that owners and reviewers in the other documents can cite a stakeholder ID. The project is a single-person course assignment with a short Business Case ([BC-001]).
|
||||
|
||||
## Stakeholder Summary Table
|
||||
|
||||
| ID | Name | Role/Title | Organization | Power Level | Interest Level | Quadrant | Primary Concern (Business Language) |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| S01 | Jens Tirsvad Nielsen | Product Owner | Tirsvad | HIGH | HIGH | Manage Closely | A working, tested, documented console calculator that matches the lecture |
|
||||
|
||||
## Power/Interest Classification Rationale
|
||||
|
||||
S01 decides scope, does the work and accepts every milestone, so S01 is managed closely.
|
||||
|
||||
## Primary Concerns and FURPS+ Mapping
|
||||
|
||||
| ID | Concern | FURPS+ attribute |
|
||||
| --- | --- | --- |
|
||||
| S01 | Operations stored in a dictionary, continue with result, restart by recursion | Functionality |
|
||||
| S01 | Simple setup with a local venv and no runtime dependencies | Supportability |
|
||||
|
||||
## Communication Requirements
|
||||
|
||||
| ID | Channel | Frequency | Deliverable | Phase / Milestone |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| S01 | Chat | Per milestone | Working-tree changes and summary | MIL-001 to MIL-004 |
|
||||
|
||||
## Conflicting Interests and Mitigations
|
||||
|
||||
| Conflict | Stakeholders | Mitigation |
|
||||
| --- | --- | --- |
|
||||
| None, there is one stakeholder | S01 | Not applicable |
|
||||
|
||||
## Traceability Analysis
|
||||
|
||||
### Business Goal Alignment
|
||||
|
||||
| Stakeholder | Concern | Business Case objective |
|
||||
| --- | --- | --- |
|
||||
| S01 | Finish the day 10 assignment | Objectives 1 to 4 in [BC-001] |
|
||||
|
||||
## Sign-Off
|
||||
|
||||
Accepted by S01.
|
||||
|
||||
---
|
||||
|
||||
[BC-001]: ./business-case.md
|
||||
[8ccefff]: https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/010-calc/commit/8ccefffef7ec1f9ecbf548085a5c92fae75e1341
|
||||
Submodule
+1
Submodule framework added at 14d221ec1c
@@ -0,0 +1,25 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=69"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "calc"
|
||||
version = "0.1.0"
|
||||
description = "A console calculator (Udemy 100 Days of Code, day 10)."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=8"]
|
||||
|
||||
[project.scripts]
|
||||
calc = "calc.calculator:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
@@ -0,0 +1,4 @@
|
||||
"""!
|
||||
@file __init__.py
|
||||
@brief Console calculator package (Udemy 100 Days of Code, day 10).
|
||||
"""
|
||||
@@ -0,0 +1,9 @@
|
||||
"""!
|
||||
@file __main__.py
|
||||
@brief Allows running the calculator with `python -m calc`.
|
||||
"""
|
||||
|
||||
from calc.calculator import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,112 @@
|
||||
"""!
|
||||
@file calculator.py
|
||||
@brief The interactive console calculator.
|
||||
"""
|
||||
|
||||
from calc.constants import (
|
||||
ANSWER_CONTINUE,
|
||||
ANSWER_NEW,
|
||||
ANSWER_QUIT,
|
||||
ASCII_TITLE,
|
||||
MESSAGE_DIVIDE_BY_ZERO,
|
||||
MESSAGE_GOODBYE,
|
||||
MESSAGE_INVALID_ANSWER,
|
||||
MESSAGE_INVALID_NUMBER,
|
||||
MESSAGE_INVALID_OPERATION,
|
||||
PROMPT_AGAIN,
|
||||
PROMPT_FIRST_NUMBER,
|
||||
PROMPT_NEXT_NUMBER,
|
||||
PROMPT_OPERATION,
|
||||
)
|
||||
from calc.operations import OPERATIONS
|
||||
|
||||
|
||||
def format_number(value: float) -> str:
|
||||
"""!
|
||||
@brief Format a number without a trailing ".0" for whole values.
|
||||
@param value The number to format.
|
||||
@return The text to show the user.
|
||||
"""
|
||||
return str(int(value)) if value.is_integer() else str(value)
|
||||
|
||||
|
||||
def read_number(prompt: str) -> float:
|
||||
"""!
|
||||
@brief Ask for a number until the user types a valid one.
|
||||
@param prompt The text shown to the user.
|
||||
@return The number typed by the user.
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
return float(input(prompt))
|
||||
except ValueError:
|
||||
print(MESSAGE_INVALID_NUMBER)
|
||||
|
||||
|
||||
def read_operation() -> str:
|
||||
"""!
|
||||
@brief Show the available operations and ask for one until it is valid.
|
||||
@return The symbol of the chosen operation, a key of OPERATIONS.
|
||||
"""
|
||||
for symbol in OPERATIONS:
|
||||
print(symbol)
|
||||
while True:
|
||||
symbol = input(PROMPT_OPERATION).strip()
|
||||
if symbol in OPERATIONS:
|
||||
return symbol
|
||||
print(MESSAGE_INVALID_OPERATION)
|
||||
|
||||
|
||||
def read_answer(result: float) -> str:
|
||||
"""!
|
||||
@brief Ask whether to continue with the result, start anew or quit.
|
||||
@param result The result of the last calculation.
|
||||
@return One of ANSWER_CONTINUE, ANSWER_NEW or ANSWER_QUIT.
|
||||
"""
|
||||
while True:
|
||||
answer = input(PROMPT_AGAIN.format(result=format_number(result)))
|
||||
answer = answer.strip().lower()
|
||||
if answer in (ANSWER_CONTINUE, ANSWER_NEW, ANSWER_QUIT):
|
||||
return answer
|
||||
print(MESSAGE_INVALID_ANSWER)
|
||||
|
||||
|
||||
def calculator() -> None:
|
||||
"""!
|
||||
@brief Run one calculation session.
|
||||
|
||||
The user may continue with the previous result. Starting a new
|
||||
calculation calls this function again (recursion), which gives a clean
|
||||
restart. A division by zero also restarts the calculator.
|
||||
"""
|
||||
num1 = read_number(PROMPT_FIRST_NUMBER)
|
||||
while True:
|
||||
symbol = read_operation()
|
||||
num2 = read_number(PROMPT_NEXT_NUMBER)
|
||||
try:
|
||||
result = OPERATIONS[symbol](num1, num2)
|
||||
except ZeroDivisionError:
|
||||
print(MESSAGE_DIVIDE_BY_ZERO)
|
||||
calculator()
|
||||
return
|
||||
print(
|
||||
f"{format_number(num1)} {symbol} {format_number(num2)} "
|
||||
f"= {format_number(result)}"
|
||||
)
|
||||
answer = read_answer(result)
|
||||
if answer == ANSWER_CONTINUE:
|
||||
num1 = result
|
||||
elif answer == ANSWER_NEW:
|
||||
calculator()
|
||||
return
|
||||
else:
|
||||
print(MESSAGE_GOODBYE)
|
||||
return
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""!
|
||||
@brief Program entry point: show the ASCII title and start the calculator.
|
||||
"""
|
||||
print(ASCII_TITLE)
|
||||
calculator()
|
||||
@@ -0,0 +1,48 @@
|
||||
"""!
|
||||
@file constants.py
|
||||
@brief Constants used by the console calculator.
|
||||
"""
|
||||
|
||||
## Operation symbols, in the order they are offered to the user.
|
||||
SYMBOL_ADD: str = "+"
|
||||
SYMBOL_SUBTRACT: str = "-"
|
||||
SYMBOL_MULTIPLY: str = "*"
|
||||
SYMBOL_DIVIDE: str = "/"
|
||||
|
||||
## Answers accepted after a calculation.
|
||||
ANSWER_CONTINUE: str = "y"
|
||||
ANSWER_NEW: str = "n"
|
||||
ANSWER_QUIT: str = "q"
|
||||
|
||||
## Prompts and messages shown to the user.
|
||||
PROMPT_FIRST_NUMBER: str = "What's the first number?: "
|
||||
PROMPT_NEXT_NUMBER: str = "What's the next number?: "
|
||||
PROMPT_OPERATION: str = "Pick an operation: "
|
||||
PROMPT_AGAIN: str = (
|
||||
f"Type '{ANSWER_CONTINUE}' to continue calculating with {{result}}, "
|
||||
f"'{ANSWER_NEW}' to start a new calculation, "
|
||||
f"or '{ANSWER_QUIT}' to quit: "
|
||||
)
|
||||
MESSAGE_INVALID_NUMBER: str = "That is not a number, please try again."
|
||||
MESSAGE_INVALID_OPERATION: str = "That is not a valid operation, please try again."
|
||||
MESSAGE_INVALID_ANSWER: str = "Please answer with one of the listed letters."
|
||||
MESSAGE_DIVIDE_BY_ZERO: str = "You cannot divide by zero."
|
||||
MESSAGE_GOODBYE: str = "Goodbye!"
|
||||
|
||||
## ASCII title shown at start: a calculator with the lettering to its right.
|
||||
ASCII_TITLE: str = r"""
|
||||
_____________________
|
||||
| _________________ |
|
||||
| | 0 | |
|
||||
| |_________________| |
|
||||
| ___ ___ ___ ___ | ____ _ _ _
|
||||
| | 7 | 8 | 9 | | + | | / ___|__ _ | | ___ _ _| | __ _| |_ ___ _ __
|
||||
| |___|___|___| |___| | | | / _` | | |/ __| | | | |/ _` | __/ _ \| '__|
|
||||
| | 4 | 5 | 6 | | - | | | |__| (_| | | | (__| |_| | | (_| | || (_) | |
|
||||
| |___|___|___| |___| | \____\__,_| |_|\___|\__,_|_|\__,_|\__\___/|_|
|
||||
| | 1 | 2 | 3 | | x | |
|
||||
| |___|___|___| |___| |
|
||||
| | . | 0 | = | | / | |
|
||||
| |___|___|___| |___| |
|
||||
|_____________________|
|
||||
"""
|
||||
@@ -0,0 +1,63 @@
|
||||
"""!
|
||||
@file operations.py
|
||||
@brief Arithmetic operations and the dictionary that maps symbols to them.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from calc.constants import (
|
||||
SYMBOL_ADD,
|
||||
SYMBOL_DIVIDE,
|
||||
SYMBOL_MULTIPLY,
|
||||
SYMBOL_SUBTRACT,
|
||||
)
|
||||
|
||||
|
||||
def add(n1: float, n2: float) -> float:
|
||||
"""!
|
||||
@brief Add two numbers.
|
||||
@param n1 The first number.
|
||||
@param n2 The second number.
|
||||
@return The sum of @p n1 and @p n2.
|
||||
"""
|
||||
return n1 + n2
|
||||
|
||||
|
||||
def subtract(n1: float, n2: float) -> float:
|
||||
"""!
|
||||
@brief Subtract the second number from the first.
|
||||
@param n1 The first number.
|
||||
@param n2 The second number.
|
||||
@return The difference @p n1 minus @p n2.
|
||||
"""
|
||||
return n1 - n2
|
||||
|
||||
|
||||
def multiply(n1: float, n2: float) -> float:
|
||||
"""!
|
||||
@brief Multiply two numbers.
|
||||
@param n1 The first number.
|
||||
@param n2 The second number.
|
||||
@return The product of @p n1 and @p n2.
|
||||
"""
|
||||
return n1 * n2
|
||||
|
||||
|
||||
def divide(n1: float, n2: float) -> float:
|
||||
"""!
|
||||
@brief Divide the first number by the second.
|
||||
@param n1 The dividend.
|
||||
@param n2 The divisor.
|
||||
@return The quotient @p n1 divided by @p n2.
|
||||
@throws ZeroDivisionError When @p n2 is zero.
|
||||
"""
|
||||
return n1 / n2
|
||||
|
||||
|
||||
## Maps an operation symbol to its function. The functions are stored, not called.
|
||||
OPERATIONS: dict[str, Callable[[float, float], float]] = {
|
||||
SYMBOL_ADD: add,
|
||||
SYMBOL_SUBTRACT: subtract,
|
||||
SYMBOL_MULTIPLY: multiply,
|
||||
SYMBOL_DIVIDE: divide,
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"""!
|
||||
@file test_calculator.py
|
||||
@brief Tests for the interactive calculator loop.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from calc.calculator import calculator, format_number, main
|
||||
|
||||
|
||||
def feed(monkeypatch: pytest.MonkeyPatch, answers: list[str]) -> None:
|
||||
"""!
|
||||
@brief Make input() return the given answers in order.
|
||||
@param monkeypatch The pytest monkeypatch fixture.
|
||||
@param answers The lines the "user" types.
|
||||
"""
|
||||
lines = iter(answers)
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": next(lines))
|
||||
|
||||
|
||||
def test_format_number_drops_trailing_zero() -> None:
|
||||
assert format_number(5.0) == "5"
|
||||
assert format_number(2.5) == "2.5"
|
||||
|
||||
|
||||
def test_calculator_adds_then_quits(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed(monkeypatch, ["2", "+", "3", "q"])
|
||||
calculator()
|
||||
assert "2 + 3 = 5" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_calculator_continues_with_previous_result(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed(monkeypatch, ["2", "+", "3", "y", "*", "4", "q"])
|
||||
calculator()
|
||||
assert "5 * 4 = 20" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_calculator_starts_new_calculation(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed(monkeypatch, ["2", "+", "3", "n", "10", "/", "4", "q"])
|
||||
calculator()
|
||||
assert "10 / 4 = 2.5" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_calculator_reprompts_on_invalid_input(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed(monkeypatch, ["abc", "1", "^", "+", "x", "1", "?", "q"])
|
||||
calculator()
|
||||
out = capsys.readouterr().out
|
||||
assert "not a number" in out
|
||||
assert "not a valid operation" in out
|
||||
assert "Please answer" in out
|
||||
assert "1 + 1 = 2" in out
|
||||
|
||||
|
||||
def test_calculator_restarts_after_division_by_zero(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed(monkeypatch, ["1", "/", "0", "6", "-", "1", "q"])
|
||||
calculator()
|
||||
out = capsys.readouterr().out
|
||||
assert "cannot divide by zero" in out
|
||||
assert "6 - 1 = 5" in out
|
||||
|
||||
|
||||
def test_main_shows_ascii_art(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed(monkeypatch, ["1", "+", "1", "q"])
|
||||
main()
|
||||
out = capsys.readouterr().out
|
||||
assert "|_____________________|" in out
|
||||
assert "Goodbye!" in out
|
||||
@@ -0,0 +1,33 @@
|
||||
"""!
|
||||
@file test_operations.py
|
||||
@brief Tests for the arithmetic operations.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from calc.operations import OPERATIONS, add, divide, multiply, subtract
|
||||
|
||||
|
||||
def test_add_returns_sum() -> None:
|
||||
assert add(2, 3) == 5
|
||||
|
||||
|
||||
def test_subtract_returns_difference() -> None:
|
||||
assert subtract(2, 3) == -1
|
||||
|
||||
|
||||
def test_multiply_returns_product() -> None:
|
||||
assert multiply(4, 2.5) == 10
|
||||
|
||||
|
||||
def test_divide_returns_quotient() -> None:
|
||||
assert divide(9, 3) == 3
|
||||
|
||||
|
||||
def test_divide_when_divisor_is_zero() -> None:
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
divide(1, 0)
|
||||
|
||||
|
||||
def test_operations_maps_symbols_to_functions() -> None:
|
||||
assert OPERATIONS == {"+": add, "-": subtract, "*": multiply, "/": divide}
|
||||
Reference in New Issue
Block a user