Commit 756d010b09
756d010b0945ac8cd7b4559ac39d9d4d9c7e9395
parent: 0d7eff3729
Verified · cmc
cmc <hello@cleberg.net> · 2026-08-23 02:18 UTC
Fix the crash, make it importable, and check it
main.py has not been touched since the 2023 initial commit and could never have
run: line 29 set recipient_emails, line 32 read recipient_email. NameError before
a single message was sent. ruff's F821 catches it in a second, which is the
argument for the workflow this adds.
It was also entirely top-level, so importing the module fetched a poem over the
network and tried to send mail. The work is now in fetch_poem, build_message and
main, behind a __main__ guard — python main.py behaves exactly as before, and
importing it does nothing.
That makes it testable: two tests cover the message headers and the PoetryDB
response shape, with the request stubbed. json was imported and never used.
The sender and recipient are still empty strings to fill in; that is
configuration, not a bug.
Layout: unified · split
.github/workflows/test.yml
added
+38
| @@ -0,0 +1,38 @@ |
| |
1 | name: Test |
| |
2 | |
| |
3 | # main.py had never run: it referenced recipient_email where the variable was |
| |
4 | # recipient_emails, so it raised NameError before sending anything. Nothing |
| |
5 | # would have caught that — ruff's F821 does, in a second. |
| |
6 | on: |
| |
7 | pull_request: |
| |
8 | push: |
| |
9 | branches: [main] |
| |
10 | |
| |
11 | permissions: |
| |
12 | contents: read |
| |
13 | |
| |
14 | jobs: |
| |
15 | test: |
| |
16 | runs-on: ubuntu-latest |
| |
17 | steps: |
| |
18 | - uses: actions/checkout@v7 |
| |
19 | |
| |
20 | - uses: actions/setup-python@v7 |
| |
21 | with: |
| |
22 | python-version: '3.11' |
| |
23 | |
| |
24 | - name: Install |
| |
25 | run: pip install -r requirements-dev.txt |
| |
26 | |
| |
27 | - uses: astral-sh/ruff-action@v3.2.2 |
| |
28 | with: |
| |
29 | version: "0.16.4" |
| |
30 | |
| |
31 | - name: Lint |
| |
32 | run: ruff check --no-fix |
| |
33 | |
| |
34 | - name: Format |
| |
35 | run: ruff format --check |
| |
36 | |
| |
37 | - name: Tests |
| |
38 | run: python -m pytest -q |
.gitignore
added
+5
| @@ -0,0 +1,5 @@ |
| |
1 | |
| |
2 | __pycache__/ |
| |
3 | *.py[cod] |
| |
4 | .pytest_cache/ |
| |
5 | .venv/ |
main.py
+44 −36
| @@ -1,39 +1,47 @@ |
| 1 | # Import Python packages |
1 | # Fetch a random poem from PoetryDB and mail it. |
| 2 | from email.mime.text import MIMEText |
| |
| 3 | import smtplib |
2 | import smtplib |
| 4 | import json |
3 | from email.mime.text import MIMEText |
| |
4 | |
| 5 | import requests |
5 | import requests |
| 6 | |
6 | |
| 7 | # Send API request for a random poem |
7 | POETRY_URL = "https://poetrydb.org/random" |
| 8 | json_data = requests.get('https://poetrydb.org/random').json() |
8 | SMTP_SERVER = "localhost" |
| 9 | |
9 | |
| 10 | # Extract the poem details from the JSON response |
10 | # Fill these in before running. |
| 11 | title = json_data[0]['title'] |
11 | SENDER_EMAIL = "" |
| 12 | author = json_data[0]['author'] |
12 | RECIPIENT_EMAILS = "" |
| 13 | line_count = json_data[0]['linecount'] |
13 | |
| 14 | lines = '' |
14 | |
| 15 | for line in json_data[0]['lines']: |
15 | def fetch_poem(url=POETRY_URL): |
| 16 | lines = lines + line + "\n" |
16 | """Return (title, author, line_count, body) for a random poem.""" |
| 17 | |
17 | json_data = requests.get(url, timeout=30).json() |
| 18 | # A test print() statement to ensure the request and parsing processed the data |
18 | poem = json_data[0] |
| 19 | # correctly |
19 | lines = "" |
| 20 | # print(title, "\n", author, "\n\n", lines) |
20 | for line in poem["lines"]: |
| 21 | |
21 | lines = lines + line + "\n" |
| 22 | msg_body = title + "\n" + author + "\n\n" + lines |
22 | return poem["title"], poem["author"], poem["linecount"], lines |
| 23 | |
23 | |
| 24 | # Create plaintext message container |
24 | |
| 25 | msg = MIMEText(msg_body) |
25 | def build_message(title, author, line_count, lines, sender, recipient): |
| 26 | |
26 | """Return a plaintext MIMEText message for one poem.""" |
| 27 | # Prepare the metadata of the message |
27 | msg = MIMEText(title + "\n" + author + "\n\n" + lines) |
| 28 | sender_email = '' |
28 | msg["Subject"] = "Your Daily Poem (" + line_count + " lines)" |
| 29 | recipient_emails = '' |
29 | msg["From"] = sender |
| 30 | msg['Subject'] = 'Your Daily Poem (' + line_count + ' lines)' |
30 | msg["To"] = recipient |
| 31 | msg['From'] = sender_email |
31 | return msg |
| 32 | msg['To'] = recipient_email |
32 | |
| 33 | |
33 | |
| 34 | # Send the message via our own SMTP server, but don't include the |
34 | def main(): |
| 35 | # envelope header. |
35 | title, author, line_count, lines = fetch_poem() |
| 36 | smtp_server = 'localhost' |
36 | msg = build_message( |
| 37 | s = smtplib.SMTP(smtp_server) |
37 | title, author, line_count, lines, SENDER_EMAIL, RECIPIENT_EMAILS |
| 38 | s.sendmail(sender_email, [recipient_emails], msg.as_string()) |
38 | ) |
| 39 | s.quit() |
39 | |
| |
40 | # Send via the local SMTP server, without the envelope header. |
| |
41 | s = smtplib.SMTP(SMTP_SERVER) |
| |
42 | s.sendmail(SENDER_EMAIL, [RECIPIENT_EMAILS], msg.as_string()) |
| |
43 | s.quit() |
| |
44 | |
| |
45 | |
| |
46 | if __name__ == "__main__": |
| |
47 | main() |
requirements-dev.in
added
+2
| @@ -0,0 +1,2 @@ |
| |
1 | requests |
| |
2 | pytest |
requirements-dev.txt
added
+22
| @@ -0,0 +1,22 @@ |
| |
1 | # This file was autogenerated by uv via the following command: |
| |
2 | # uv pip compile requirements-dev.in -o requirements-dev.txt --python-version 3.11 |
| |
3 | certifi==2026.7.22 |
| |
4 | # via requests |
| |
5 | charset-normalizer==3.5.1 |
| |
6 | # via requests |
| |
7 | idna==3.19 |
| |
8 | # via requests |
| |
9 | iniconfig==2.3.0 |
| |
10 | # via pytest |
| |
11 | packaging==26.3 |
| |
12 | # via pytest |
| |
13 | pluggy==1.6.0 |
| |
14 | # via pytest |
| |
15 | pygments==2.21.0 |
| |
16 | # via pytest |
| |
17 | pytest==9.1.1 |
| |
18 | # via -r requirements-dev.in |
| |
19 | requests==2.34.2 |
| |
20 | # via -r requirements-dev.in |
| |
21 | urllib3==2.7.0 |
| |
22 | # via requests |
tests/test_main.py
added
+47
| @@ -0,0 +1,47 @@ |
| |
1 | import unittest |
| |
2 | from unittest.mock import patch |
| |
3 | |
| |
4 | from main import build_message, fetch_poem |
| |
5 | |
| |
6 | |
| |
7 | class FakeResponse: |
| |
8 | def __init__(self, payload): |
| |
9 | self._payload = payload |
| |
10 | |
| |
11 | def json(self): |
| |
12 | return self._payload |
| |
13 | |
| |
14 | |
| |
15 | class TestBuildMessage(unittest.TestCase): |
| |
16 | def test_headers_and_body(self): |
| |
17 | msg = build_message("A Title", "An Author", "4", "one\ntwo\n", "from@x", "to@y") |
| |
18 | |
| |
19 | self.assertEqual(msg["Subject"], "Your Daily Poem (4 lines)") |
| |
20 | self.assertEqual(msg["From"], "from@x") |
| |
21 | self.assertEqual(msg["To"], "to@y") |
| |
22 | self.assertIn("A Title", msg.get_payload()) |
| |
23 | self.assertIn("An Author", msg.get_payload()) |
| |
24 | self.assertIn("one\ntwo", msg.get_payload()) |
| |
25 | |
| |
26 | |
| |
27 | class TestFetchPoem(unittest.TestCase): |
| |
28 | """PoetryDB is stubbed: the test covers the shape this code expects from it, |
| |
29 | not the service being up.""" |
| |
30 | |
| |
31 | PAYLOAD = [ |
| |
32 | { |
| |
33 | "title": "Sonnet", |
| |
34 | "author": "Anon", |
| |
35 | "linecount": "2", |
| |
36 | "lines": ["first line", "second line"], |
| |
37 | } |
| |
38 | ] |
| |
39 | |
| |
40 | def test_fields_are_extracted_and_lines_joined(self): |
| |
41 | with patch("main.requests.get", return_value=FakeResponse(self.PAYLOAD)): |
| |
42 | title, author, line_count, lines = fetch_poem() |
| |
43 | |
| |
44 | self.assertEqual(title, "Sonnet") |
| |
45 | self.assertEqual(author, "Anon") |
| |
46 | self.assertEqual(line_count, "2") |
| |
47 | self.assertEqual(lines, "first line\nsecond line\n") |