Add Caesar cipher project with Doxygen docs

This commit is contained in:
2026-10-04 13:11:15 +08:00
parent 99cccdf3da
commit 0957bff43a
12 changed files with 248 additions and 1 deletions
+3
View File
@@ -174,3 +174,6 @@ cython_debug/
# PyPI configuration file
.pypirc
# Doxygen output
docs/doxygen/
+12
View File
@@ -0,0 +1,12 @@
PROJECT_NAME = "Caesar Cipher"
PROJECT_BRIEF = "Udemy 100 Days of Code - Day 8"
OUTPUT_DIRECTORY = docs/doxygen
INPUT = src README.md docs/index.md
USE_MDFILE_AS_MAINPAGE = README.md
FILE_PATTERNS = *.py *.md
RECURSIVE = YES
EXTRACT_ALL = YES
OPTIMIZE_OUTPUT_JAVA = YES
GENERATE_HTML = YES
GENERATE_LATEX = NO
QUIET = YES
+75 -1
View File
@@ -1,2 +1,76 @@
# 008-caesar_cipher
# 🔐 Caesar Cipher
A beginner-friendly Python program that encodes messages with the Caesar cipher.
## 📚 Table of Contents
- [Overview](#-overview)
- [Requirements](#-requirements)
- [Setup](#-setup)
- [Run](#-run)
- [Tests](#-tests)
- [License](#-license)
- [Links](#-links)
## 🧭 Overview
The Caesar cipher shifts every letter by a fixed number of places (with a shift of 3, `E` becomes `H`). This project, from Udemy's *100 Days of Code: The Complete Python Pro Bootcamp* (Day 8), finds letter positions with `list.index` and wraps around the alphabet with the modulo operator. Part 1 covers encryption; decryption and a combined program follow in later parts. Code lives in `src/`, tests in `tests/`, docs in `docs/`.
## 📋 Requirements
- Python 3.13 or newer
- `pytest` (development only; no runtime dependencies)
- Optional: [Doxygen](https://www.doxygen.nl/) to build the API docs
## 🛠️ Setup
Create and activate a local virtual environment, then upgrade pip and install the project:
```bash
python -m venv .venv
```
Activate it:
```bash
# Linux / macOS
source .venv/bin/activate
# Git Bash on Windows
source .venv/Scripts/activate
```
```powershell
# Windows PowerShell
.venv\Scripts\Activate.ps1
```
```bash
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```
## ▶️ Run
```bash
python -m caesar_cipher
```
or, after installation, `caesar-cipher`.
## 🧪 Tests
```bash
python -m pytest
```
Build the API docs with `doxygen Doxyfile` (output in `docs/doxygen/html`).
## 📄 License
GNU AGPL-3.0 – see [LICENSE](LICENSE).
## 🔗 Links
- [Repository](https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code)
- [Documentation](docs/index.md)
- [Issue tracker](https://git.tirsystem.com/Tirsvad-Udemy-100_days_of_code/008-caesar_cipher/issues)
+9
View File
@@ -0,0 +1,9 @@
# Caesar Cipher – documentation
Each letter is moved `shift` places through the alphabet:
`new_position = (ALPHABET.index(letter) + shift) % 26`.
The modulo keeps positions in range for shifts past `z`.
Non-letters are left untouched and letter case is preserved.
API reference: run `doxygen Doxyfile` from the repository root and open
`docs/doxygen/html/index.html`. The step-by-step plan is in [plan.md](plan.md).
+26
View File
@@ -0,0 +1,26 @@
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
[project]
name = "caesar-cipher"
version = "0.1.0"
description = "A beginner-friendly Caesar cipher encoder/decoder (Udemy 100 Days of Code, Day 8)."
readme = "README.md"
requires-python = ">=3.13"
license = "AGPL-3.0-or-later"
license-files = ["LICENSE"]
dependencies = []
[project.optional-dependencies]
dev = ["pytest>=8"]
[project.scripts]
caesar-cipher = "caesar_cipher.main:main"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
+6
View File
@@ -0,0 +1,6 @@
## @file __init__.py
# @brief Public API of the caesar_cipher package.
from caesar_cipher.cipher import encrypt
__all__ = ["encrypt"]
+6
View File
@@ -0,0 +1,6 @@
## @file __main__.py
# @brief Enables `python -m caesar_cipher`.
from caesar_cipher.main import main
main()
+21
View File
@@ -0,0 +1,21 @@
## @file cipher.py
# @brief Caesar cipher encryption.
from caesar_cipher.constants import ALPHABET
def encrypt(text: str, shift: int) -> str:
## @brief Encrypt a text with the Caesar cipher.
# @param text Original text.
# @param shift Positions to move each letter (may be negative or > 26).
# @return The encrypted text; non-letters are kept, case is preserved.
result = []
for char in text:
lower = char.lower()
if lower in ALPHABET:
new_position = (ALPHABET.index(lower) + shift) % len(ALPHABET)
new_char = ALPHABET[new_position]
result.append(new_char.upper() if char.isupper() else new_char)
else:
result.append(char)
return "".join(result)
+13
View File
@@ -0,0 +1,13 @@
## @file constants.py
# @brief Constants used by the Caesar cipher.
## Lowercase alphabet used to look up letter positions.
ALPHABET = list("abcdefghijklmnopqrstuvwxyz")
## Prompt for the message.
PROMPT_TEXT = "Type your message:\n"
## Prompt for the shift number.
PROMPT_SHIFT = "Type the shift number:\n"
## Name of the environment file holding tokens.
ENV_FILE = ".env"
+31
View File
@@ -0,0 +1,31 @@
## @file env.py
# @brief Minimal stdlib `.env` reader (no third-party dependency).
import os
from pathlib import Path
from caesar_cipher.constants import ENV_FILE
def load_env(path: str | Path = ENV_FILE) -> dict[str, str]:
## @brief Read KEY=VALUE pairs from an env file.
# @param path Path of the env file.
# @return Mapping of variables; empty if the file does not exist.
values: dict[str, str] = {}
env_path = Path(path)
if not env_path.is_file():
return values
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
values[key.strip()] = value.strip().strip("\"'")
return values
def get_token(name: str, path: str | Path = ENV_FILE) -> str | None:
## @brief Get a token from the process environment or the env file.
# @param name Variable name, e.g. GITEA_TOKEN.
# @return The token or None when not set.
return os.environ.get(name) or load_env(path).get(name)
+20
View File
@@ -0,0 +1,20 @@
## @file main.py
# @brief Interactive command line interface.
from caesar_cipher import constants as c
from caesar_cipher.cipher import encrypt
def main() -> None:
## @brief Ask for a message and a shift, then print the encrypted text.
text = input(c.PROMPT_TEXT)
try:
shift = int(input(c.PROMPT_SHIFT))
except ValueError:
print("The shift must be a whole number.")
return
print(f"Here's the encoded result: {encrypt(text, shift)}")
if __name__ == "__main__":
main()
+26
View File
@@ -0,0 +1,26 @@
from caesar_cipher import encrypt
from caesar_cipher.env import load_env
def test_encrypt_basic():
assert encrypt("hello", 5) == "mjqqt"
def test_wraps_around_alphabet():
assert encrypt("xyz", 3) == "abc"
assert encrypt("abc", 29) == "def"
def test_non_letters_and_case_preserved():
assert encrypt("Hi there, 42!", 1) == "Ij uifsf, 42!"
def test_negative_shift():
assert encrypt("def", -3) == "abc"
def test_load_env(tmp_path):
f = tmp_path / ".env"
f.write_text("# c\nA=1\nB='two'\n", encoding="utf-8")
assert load_env(f) == {"A": "1", "B": "two"}
assert load_env(tmp_path / "missing") == {}