6 Commits
Author SHA1 Message Date
Tirsvad 16cdc0b08b Merge pull request 'Add screenshot of the running app to README' (#4) from docs-screenshot into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Reviewed-on: #4
2026-10-04 07:31:54 +02:00
Tirsvad c89da47d2a Add terminal screenshot to README
- Insert SVG screenshot showing encode/decode workflow
- Link screenshot in Run section of README
2026-10-04 13:31:09 +08:00
Tirsvad 27952755d3 Merge pull request 'Part 3: Reorganising the code' (#3) from part-3-reorganising-code into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Reviewed-on: #3
2026-10-04 07:26:15 +02:00
Tirsvad 9d8beb3aef Refactor to unified caesar() function with encode/decode support
- Replace separate encrypt/decrypt with shared caesar() function
- Add direction constants (DIRECTION_ENCODE, DIRECTION_DECODE) and
  prompts
- Update main.py to support interactive encode/decode loop
- Keep encrypt/decrypt as wrappers for backward compatibility
- Add tests for caesar() with both directions and error handling
- Update tests for main.py with interactive scenario tests
2026-10-04 13:23:47 +08:00
Tirsvad d4b3e6ab19 Merge pull request 'Part 2: Decryption' (#2) from part-2-decryption into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
Reviewed-on: #2
2026-10-04 07:20:21 +02:00
Tirsvad 621f5f7272 Add decrypt function to Caesar cipher
- Implement decrypt function that reverses encryption with negative
  shift
- Export decrypt in package's public API
- Update tests with basic roundtrip and edge case tests
- Update documentation to reflect encryption/decryption support
2026-10-04 13:18:33 +08:00
9 changed files with 271 additions and 107 deletions
+78 -76
View File
@@ -1,76 +1,78 @@
# 🔐 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)
# 🔐 Caesar Cipher
A beginner-friendly Python program that encodes and decodes 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. The interactive program lets you encode or decode as many messages as you like. 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`.
![Caesar cipher running in a terminal](docs/images/screenshot.svg)
## 🧪 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)
+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="700" viewBox="0 0 760 700">
<rect width="760" height="700" rx="8" fill="#0d1117"/>
<circle cx="22" cy="20" r="6" fill="#ff5f56"/><circle cx="42" cy="20" r="6" fill="#ffbd2e"/><circle cx="62" cy="20" r="6" fill="#27c93f"/>
<g font-family="Consolas,Menlo,monospace" font-size="14" xml:space="preserve">
<text x="20" y="50" fill="#56d4dd"> ____ _ _____ ____ _ ____</text>
<text x="20" y="70" fill="#56d4dd"> / ___| / \ | ____/ ___| / \ | _ \</text>
<text x="20" y="90" fill="#56d4dd">| | / _ \ | _| \___ \ / _ \ | |_) |</text>
<text x="20" y="110" fill="#56d4dd">| |___ / ___ \| |___ ___) / ___ \| _ &lt;</text>
<text x="20" y="130" fill="#56d4dd"> \____/_/ \_\_____|____/_/ \_\_| \_\</text>
<text x="20" y="150" fill="#56d4dd"> ____ ___ ____ _ _ _____ ____</text>
<text x="20" y="170" fill="#56d4dd"> / ___|_ _| _ \| | | | ____| _ \</text>
<text x="20" y="190" fill="#56d4dd">| | | || |_) | |_| | _| | |_) |</text>
<text x="20" y="210" fill="#56d4dd">| |___ | || __/| _ | |___| _ &lt;</text>
<text x="20" y="230" fill="#56d4dd"> \____|___|_| |_| |_|_____|_| \_\</text>
<text x="20" y="250" fill="#f0c674"></text>
<text x="20" y="270" fill="#c9d1d9">Type &#x27;encode&#x27; to encrypt, type &#x27;decode&#x27; to decrypt:</text>
<text x="20" y="290" fill="#7ee787">encode</text>
<text x="20" y="310" fill="#c9d1d9">Type your message:</text>
<text x="20" y="330" fill="#7ee787">Hello, World!</text>
<text x="20" y="350" fill="#c9d1d9">Type the shift number:</text>
<text x="20" y="370" fill="#7ee787">3</text>
<text x="20" y="390" fill="#f0c674">Here&#x27;s the encoded result: Khoor, Zruog!</text>
<text x="20" y="410" fill="#c9d1d9">Type &#x27;yes&#x27; if you want to go again. Otherwise type &#x27;no&#x27;.</text>
<text x="20" y="430" fill="#7ee787">yes</text>
<text x="20" y="450" fill="#c9d1d9">Type &#x27;encode&#x27; to encrypt, type &#x27;decode&#x27; to decrypt:</text>
<text x="20" y="470" fill="#7ee787">decode</text>
<text x="20" y="490" fill="#c9d1d9">Type your message:</text>
<text x="20" y="510" fill="#7ee787">Khoor, Zruog!</text>
<text x="20" y="530" fill="#c9d1d9">Type the shift number:</text>
<text x="20" y="550" fill="#7ee787">3</text>
<text x="20" y="570" fill="#f0c674">Here&#x27;s the decoded result: Hello, World!</text>
<text x="20" y="590" fill="#c9d1d9">Type &#x27;yes&#x27; if you want to go again. Otherwise type &#x27;no&#x27;.</text>
<text x="20" y="610" fill="#7ee787">no</text>
<text x="20" y="630" fill="#f0c674">Goodbye</text>
</g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+1 -1
View File
@@ -3,7 +3,7 @@
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.
Decoding shifts by `-shift`; one shared `caesar(text, shift, direction)` function does both. 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).
+2 -2
View File
@@ -1,6 +1,6 @@
## @file __init__.py
# @brief Public API of the caesar_cipher package.
from caesar_cipher.cipher import encrypt
from caesar_cipher.cipher import caesar, decrypt, encrypt
__all__ = ["encrypt"]
__all__ = ["caesar", "encrypt", "decrypt"]
+30 -7
View File
@@ -1,14 +1,21 @@
## @file cipher.py
# @brief Caesar cipher encryption.
# @brief Caesar cipher: one shared function for encoding and decoding.
from caesar_cipher.constants import ALPHABET
from caesar_cipher.constants import ALPHABET, DIRECTION_DECODE, DIRECTION_ENCODE
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.
def caesar(text: str, shift: int, direction: str) -> str:
## @brief Encode or decode a text with the Caesar cipher.
# @param text Text to transform.
# @param shift Positions to move each letter (may be negative or > 26).
# @param direction DIRECTION_ENCODE or DIRECTION_DECODE.
# @return The transformed text; non-letters are kept, case is preserved.
# @throws ValueError If direction is not a known direction.
if direction == DIRECTION_DECODE:
shift = -shift
elif direction != DIRECTION_ENCODE:
raise ValueError(f"Unknown direction: {direction!r}")
result = []
for char in text:
lower = char.lower()
@@ -19,3 +26,19 @@ def encrypt(text: str, shift: int) -> str:
else:
result.append(char)
return "".join(result)
def encrypt(text: str, shift: int) -> str:
## @brief Encrypt a text with the Caesar cipher.
# @param text Original text.
# @param shift Number of positions to shift forward.
# @return The encrypted text.
return caesar(text, shift, DIRECTION_ENCODE)
def decrypt(text: str, shift: int) -> str:
## @brief Decrypt a Caesar-cipher text.
# @param text Encrypted text.
# @param shift Shift that was used to encrypt.
# @return The original text.
return caesar(text, shift, DIRECTION_DECODE)
+28
View File
@@ -4,10 +4,38 @@
## Lowercase alphabet used to look up letter positions.
ALPHABET = list("abcdefghijklmnopqrstuvwxyz")
## Direction keyword for encoding.
DIRECTION_ENCODE = "encode"
## Direction keyword for decoding.
DIRECTION_DECODE = "decode"
## Prompt for the direction.
PROMPT_DIRECTION = "Type 'encode' to encrypt, type 'decode' to decrypt:\n"
## Prompt for the message.
PROMPT_TEXT = "Type your message:\n"
## Prompt for the shift number.
PROMPT_SHIFT = "Type the shift number:\n"
## Prompt to continue.
PROMPT_AGAIN = "Type 'yes' if you want to go again. Otherwise type 'no'.\n"
## Name of the environment file holding tokens.
ENV_FILE = ".env"
## ASCII art title shown at start-up.
TITLE = r"""
____ _ _____ ____ _ ____
/ ___| / \ | ____/ ___| / \ | _ \
| | / _ \ | _| \___ \ / _ \ | |_) |
| |___ / ___ \| |___ ___) / ___ \| _ <
\____/_/ \_\_____|____/_/ \_\_| \_\
____ ___ ____ _ _ _____ ____
/ ___|_ _| _ \| | | | ____| _ \
| | | || |_) | |_| | _| | |_) |
| |___ | || __/| _ | |___| _ <
\____|___|_| |_| |_|_____|_| \_\
"""
## ANSI escape code for bright cyan text.
COLOR_TITLE = "\033[96m"
## ANSI escape code that resets text colour.
COLOR_RESET = "\033[0m"
+43 -20
View File
@@ -1,20 +1,43 @@
## @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()
## @file main.py
# @brief Interactive command line interface.
import os
import sys
from caesar_cipher import constants as c
from caesar_cipher.cipher import caesar
def print_title() -> None:
## @brief Print the ASCII title, in colour when the terminal supports it.
if sys.stdout.isatty() and "NO_COLOR" not in os.environ:
os.system("") # enables ANSI escape sequences on older Windows consoles
print(f"{c.COLOR_TITLE}{c.TITLE}{c.COLOR_RESET}")
else:
print(c.TITLE)
def main() -> None:
## @brief Run the interactive encode/decode loop until the user stops.
print_title()
while True:
direction = input(c.PROMPT_DIRECTION).strip().lower()
if direction not in (c.DIRECTION_ENCODE, c.DIRECTION_DECODE):
print("Please type 'encode' or 'decode'.")
continue
text = input(c.PROMPT_TEXT)
try:
shift = int(input(c.PROMPT_SHIFT))
except ValueError:
print("The shift must be a whole number.")
continue
print(f"Here's the {direction}d result: {caesar(text, shift, direction)}")
if input(c.PROMPT_AGAIN).strip().lower() != "yes":
print("Goodbye")
break
if __name__ == "__main__":
main()
+26 -1
View File
@@ -1,4 +1,6 @@
from caesar_cipher import encrypt
import pytest
from caesar_cipher import caesar, decrypt, encrypt
from caesar_cipher.env import load_env
@@ -24,3 +26,26 @@ def test_load_env(tmp_path):
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") == {}
def test_decrypt_basic():
assert decrypt("mjqqt", 5) == "hello"
def test_decrypt_roundtrip():
assert decrypt(encrypt("Hello, World!", 7), 7) == "Hello, World!"
def test_decrypt_wraps_around_alphabet():
assert decrypt("abc", 3) == "xyz"
assert decrypt("def", 29) == "abc"
def test_caesar_encode_and_decode():
assert caesar("hello", 5, "encode") == "mjqqt"
assert caesar("mjqqt", 5, "decode") == "hello"
def test_caesar_unknown_direction():
with pytest.raises(ValueError):
caesar("hello", 5, "sideways")
+28
View File
@@ -0,0 +1,28 @@
from caesar_cipher.main import main
def run(monkeypatch, capsys, answers):
it = iter(answers)
monkeypatch.setattr("builtins.input", lambda _="": next(it))
main()
return capsys.readouterr().out
def test_encode_then_stop(monkeypatch, capsys):
out = run(monkeypatch, capsys, ["encode", "hello", "5", "no"])
assert "mjqqt" in out and "Goodbye" in out
def test_decode_then_go_again(monkeypatch, capsys):
out = run(monkeypatch, capsys, ["decode", "mjqqt", "5", "yes", "encode", "a", "1", "no"])
assert "hello" in out and "b" in out
def test_invalid_input_reprompts(monkeypatch, capsys):
out = run(monkeypatch, capsys, ["nope", "decode", "x", "abc", "encode", "a", "1", "no"])
assert "encode' or 'decode" in out and "whole number" in out
def test_title_printed_plain_when_not_a_tty(monkeypatch, capsys):
out = run(monkeypatch, capsys, ["encode", "a", "1", "no"])
assert "____" in out and "\033[" not in out