0 Where the release starts

Read the tutorial. Build it yourself is the other way to read this part.

just setup

tally counts the words of a text, and two files of tests check it. Run the tests, to see where we begin.

uv run python -m unittest discover -s tests -q

All three tests pass.

What changed

  • .gitignore: New, 10 lines. The file.
  • .python-version: New, 1 line. The file.
  • README.md: New, 3 lines. The file.
  • justfile: adds recipe default, recipe run, recipe setup, recipe test. New, 15 lines. The file.
  • pyproject.toml: New, 14 lines. The file.
  • src/tally/__init__.py: adds count, main. New, 16 lines. The file.
  • src/tally/__main__.py: New, 3 lines. The file.
  • tests/test_count.py: adds class Count, test_case, test_words. New, 11 lines. The file.
  • tests/test_empty.py: adds class Empty, test_empty. New, 8 lines. The file.
  • uv.lock: New, 8 lines. The file.
The diff of .gitignore
@@ -0,0 +1,10 @@
+# Python-generated files
+__pycache__/
+*.py[oc]
+build/
+dist/
+wheels/
+*.egg-info
+
+# Virtual environments
+.venv
The diff of .python-version
@@ -0,0 +1 @@
+3.12
The diff of README.md
@@ -0,0 +1,3 @@
+# tally
+
+Count the words in a text.
The diff of justfile
@@ -0,0 +1,15 @@
+# The recipes of tally. Each one runs through uv, in the project's own environment.
+default:
+    @just --list
+
+# Make the environment of this step: the first command of every step
+setup:
+    uv sync
+
+# Count the words of a file
+run file:
+    uv run tally {{ file }}
+
+# Run the tests
+test:
+    uv run python -m unittest discover -s tests -q
The diff of pyproject.toml
@@ -0,0 +1,14 @@
+[project]
+name = "tally"
+version = "0.1.0"
+description = "Count the words in a text"
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = []
+
+[project.scripts]
+tally = "tally:main"
+
+[build-system]
+requires = ["uv_build>=0.12.13,<0.13.0"]
+build-backend = "uv_build"
The diff of src/tally/__init__.py
@@ -0,0 +1,16 @@
+"""tally: count the words in a text."""
+
+import sys
+
+
+def count(text):
+    counts = {}
+    for word in text.lower().split():
+        counts[word] = counts.get(word, 0) + 1
+    return counts
+
+
+def main() -> None:
+    text = open(sys.argv[1]).read() if len(sys.argv) > 1 else sys.stdin.read()
+    for word, n in sorted(count(text).items()):
+        print(n, word)
The diff of src/tally/__main__.py
@@ -0,0 +1,3 @@
+from tally import main
+
+main()
The diff of tests/test_count.py
@@ -0,0 +1,11 @@
+import unittest
+
+from tally import count
+
+
+class Count(unittest.TestCase):
+    def test_words(self):
+        self.assertEqual(count("a b a"), {"a": 2, "b": 1})
+
+    def test_case(self):
+        self.assertEqual(count("A a"), {"a": 2})
The diff of tests/test_empty.py
@@ -0,0 +1,8 @@
+import unittest
+
+from tally import count
+
+
+class Empty(unittest.TestCase):
+    def test_empty(self):
+        self.assertEqual(count(""), {})

The code.