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 @@
1name: 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.
6on:
7 pull_request:
8 push:
9 branches: [main]
10
11permissions:
12 contents: read
13
14jobs:
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.
2from email.mime.text import MIMEText
3import smtplib 2import smtplib
4import json 3from email.mime.text import MIMEText
4
5import requests 5import requests
6 6
7# Send API request for a random poem 7POETRY_URL = "https://poetrydb.org/random"
8json_data = requests.get('https://poetrydb.org/random').json() 8SMTP_SERVER = "localhost"
9 9
10# Extract the poem details from the JSON response 10# Fill these in before running.
11title = json_data[0]['title'] 11SENDER_EMAIL = ""
12author = json_data[0]['author'] 12RECIPIENT_EMAILS = ""
13line_count = json_data[0]['linecount'] 13
14lines = '' 14
15for line in json_data[0]['lines']: 15def 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"
22msg_body = title + "\n" + author + "\n\n" + lines 22 return poem["title"], poem["author"], poem["linecount"], lines
23 23
24# Create plaintext message container 24
25msg = MIMEText(msg_body) 25def 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)
28sender_email = '' 28 msg["Subject"] = "Your Daily Poem (" + line_count + " lines)"
29recipient_emails = '' 29 msg["From"] = sender
30msg['Subject'] = 'Your Daily Poem (' + line_count + ' lines)' 30 msg["To"] = recipient
31msg['From'] = sender_email 31 return msg
32msg['To'] = recipient_email 32
33 33
34# Send the message via our own SMTP server, but don't include the 34def main():
35# envelope header. 35 title, author, line_count, lines = fetch_poem()
36smtp_server = 'localhost' 36 msg = build_message(
37s = smtplib.SMTP(smtp_server) 37 title, author, line_count, lines, SENDER_EMAIL, RECIPIENT_EMAILS
38s.sendmail(sender_email, [recipient_emails], msg.as_string()) 38 )
39s.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
46if __name__ == "__main__":
47 main()
requirements-dev.in added +2
@@ -0,0 +1,2 @@
1requests
2pytest
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
3certifi==2026.7.22
4 # via requests
5charset-normalizer==3.5.1
6 # via requests
7idna==3.19
8 # via requests
9iniconfig==2.3.0
10 # via pytest
11packaging==26.3
12 # via pytest
13pluggy==1.6.0
14 # via pytest
15pygments==2.21.0
16 # via pytest
17pytest==9.1.1
18 # via -r requirements-dev.in
19requests==2.34.2
20 # via -r requirements-dev.in
21urllib3==2.7.0
22 # via requests
tests/test_main.py added +47
@@ -0,0 +1,47 @@
1import unittest
2from unittest.mock import patch
3
4from main import build_message, fetch_poem
5
6
7class FakeResponse:
8 def __init__(self, payload):
9 self._payload = payload
10
11 def json(self):
12 return self._payload
13
14
15class 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
27class 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")