From 0957bff43ac8b6e654db010cb9e4df21dd197629 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Sun, 4 Oct 2026 13:11:15 +0800 Subject: [PATCH] Add Caesar cipher project with Doxygen docs --- .gitignore | 3 ++ Doxyfile | 12 ++++++ README.md | 76 +++++++++++++++++++++++++++++++++- docs/index.md | 9 ++++ pyproject.toml | 26 ++++++++++++ src/caesar_cipher/__init__.py | 6 +++ src/caesar_cipher/__main__.py | 6 +++ src/caesar_cipher/cipher.py | 21 ++++++++++ src/caesar_cipher/constants.py | 13 ++++++ src/caesar_cipher/env.py | 31 ++++++++++++++ src/caesar_cipher/main.py | 20 +++++++++ tests/test_cipher.py | 26 ++++++++++++ 12 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 Doxyfile create mode 100644 docs/index.md create mode 100644 pyproject.toml create mode 100644 src/caesar_cipher/__init__.py create mode 100644 src/caesar_cipher/__main__.py create mode 100644 src/caesar_cipher/cipher.py create mode 100644 src/caesar_cipher/constants.py create mode 100644 src/caesar_cipher/env.py create mode 100644 src/caesar_cipher/main.py create mode 100644 tests/test_cipher.py diff --git a/.gitignore b/.gitignore index 36b13f1..ef1a1da 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,6 @@ cython_debug/ # PyPI configuration file .pypirc + +# Doxygen output +docs/doxygen/ diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..b406f74 --- /dev/null +++ b/Doxyfile @@ -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 diff --git a/README.md b/README.md index fdc9693..9f04ed7 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..f24a920 --- /dev/null +++ b/docs/index.md @@ -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). diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..96bc98d --- /dev/null +++ b/pyproject.toml @@ -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"] diff --git a/src/caesar_cipher/__init__.py b/src/caesar_cipher/__init__.py new file mode 100644 index 0000000..4c507e6 --- /dev/null +++ b/src/caesar_cipher/__init__.py @@ -0,0 +1,6 @@ +## @file __init__.py +# @brief Public API of the caesar_cipher package. + +from caesar_cipher.cipher import encrypt + +__all__ = ["encrypt"] diff --git a/src/caesar_cipher/__main__.py b/src/caesar_cipher/__main__.py new file mode 100644 index 0000000..d40f03f --- /dev/null +++ b/src/caesar_cipher/__main__.py @@ -0,0 +1,6 @@ +## @file __main__.py +# @brief Enables `python -m caesar_cipher`. + +from caesar_cipher.main import main + +main() diff --git a/src/caesar_cipher/cipher.py b/src/caesar_cipher/cipher.py new file mode 100644 index 0000000..738283e --- /dev/null +++ b/src/caesar_cipher/cipher.py @@ -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) diff --git a/src/caesar_cipher/constants.py b/src/caesar_cipher/constants.py new file mode 100644 index 0000000..611caf7 --- /dev/null +++ b/src/caesar_cipher/constants.py @@ -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" diff --git a/src/caesar_cipher/env.py b/src/caesar_cipher/env.py new file mode 100644 index 0000000..26e20f1 --- /dev/null +++ b/src/caesar_cipher/env.py @@ -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) diff --git a/src/caesar_cipher/main.py b/src/caesar_cipher/main.py new file mode 100644 index 0000000..6c84d9d --- /dev/null +++ b/src/caesar_cipher/main.py @@ -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() diff --git a/tests/test_cipher.py b/tests/test_cipher.py new file mode 100644 index 0000000..9a631df --- /dev/null +++ b/tests/test_cipher.py @@ -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") == {}